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.ide.common.layout.LayoutConstants.ATTR_ID; 20 import static com.android.ide.eclipse.adt.AdtConstants.DOT_PNG; 21 import static com.android.ide.eclipse.adt.AdtConstants.DOT_XML; 22 23 import com.android.annotations.NonNull; 24 import com.android.ide.common.api.IAttributeInfo; 25 import com.android.ide.common.api.IAttributeInfo.Format; 26 import com.android.ide.common.rendering.api.ResourceValue; 27 import com.android.ide.common.resources.ResourceRepository; 28 import com.android.ide.common.resources.ResourceResolver; 29 import com.android.ide.eclipse.adt.AdtPlugin; 30 import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; 31 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; 32 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; 33 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService; 34 import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; 35 import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; 36 import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; 37 import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog; 38 import com.android.ide.eclipse.adt.internal.ui.ResourceChooser; 39 import com.android.ide.eclipse.adt.internal.ui.ResourcePreviewHelper; 40 import com.android.resources.ResourceType; 41 import com.google.common.collect.Maps; 42 43 import org.eclipse.core.resources.IProject; 44 import org.eclipse.core.runtime.CoreException; 45 import org.eclipse.core.runtime.QualifiedName; 46 import org.eclipse.jface.window.Window; 47 import org.eclipse.swt.graphics.Color; 48 import org.eclipse.swt.graphics.GC; 49 import org.eclipse.swt.graphics.Image; 50 import org.eclipse.swt.graphics.ImageData; 51 import org.eclipse.swt.graphics.Point; 52 import org.eclipse.swt.graphics.RGB; 53 import org.eclipse.swt.widgets.Shell; 54 import org.eclipse.wb.draw2d.IColorConstants; 55 import org.eclipse.wb.internal.core.model.property.Property; 56 import org.eclipse.wb.internal.core.model.property.editor.AbstractTextPropertyEditor; 57 import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation; 58 import org.eclipse.wb.internal.core.model.property.editor.presentation.PropertyEditorPresentation; 59 import org.eclipse.wb.internal.core.model.property.table.PropertyTable; 60 import org.eclipse.wb.internal.core.utils.ui.DrawUtils; 61 62 import java.awt.image.BufferedImage; 63 import java.io.File; 64 import java.io.IOException; 65 import java.util.ArrayList; 66 import java.util.EnumSet; 67 import java.util.List; 68 import java.util.Map; 69 70 import javax.imageio.ImageIO; 71 72 /** 73 * Special property editor used for the {@link XmlProperty} instances which handles 74 * editing the XML properties, rendering defaults by looking up the actual colors and images, 75 */ 76 class XmlPropertyEditor extends AbstractTextPropertyEditor { 77 public static final XmlPropertyEditor INSTANCE = new XmlPropertyEditor(); 78 private static final int SAMPLE_SIZE = 10; 79 private static final int SAMPLE_MARGIN = 3; 80 XmlPropertyEditor()81 protected XmlPropertyEditor() { 82 } 83 84 private final PropertyEditorPresentation mPresentation = 85 new ButtonPropertyEditorPresentation() { 86 @Override 87 protected void onClick(PropertyTable propertyTable, Property property) throws Exception { 88 openDialog(propertyTable, property); 89 } 90 }; 91 92 @Override getPresentation()93 public PropertyEditorPresentation getPresentation() { 94 return mPresentation; 95 } 96 97 @Override getText(Property property)98 public String getText(Property property) throws Exception { 99 Object value = property.getValue(); 100 if (value instanceof String) { 101 return (String) value; 102 } 103 return null; 104 } 105 106 @Override getEditorText(Property property)107 protected String getEditorText(Property property) throws Exception { 108 return getText(property); 109 } 110 111 @Override paint(Property property, GC gc, int x, int y, int width, int height)112 public void paint(Property property, GC gc, int x, int y, int width, int height) 113 throws Exception { 114 String text = getText(property); 115 if (text != null) { 116 ResourceValue resValue = null; 117 String resolvedText = null; 118 119 // TODO: Use the constants for @, ?, @android: etc 120 if (text.startsWith("@") || text.startsWith("?")) { //$NON-NLS-1$ //$NON-NLS-2$ 121 // Yes, try to resolve it in order to show better info 122 XmlProperty xmlProperty = (XmlProperty) property; 123 ResourceResolver resolver = xmlProperty.getGraphicalEditor().getResourceResolver(); 124 boolean isFramework = text.startsWith("@android:") || text.startsWith("?android:"); 125 resValue = resolver.findResValue(text, isFramework); 126 while (resValue != null && resValue.getValue() != null) { 127 String value = resValue.getValue(); 128 if (value.startsWith("@") || value.startsWith("?")) { 129 // TODO: do I have to strip off the @ too? 130 isFramework = isFramework || value.startsWith("@android:") || value.startsWith("?android:");; 131 ResourceValue v = resolver.findResValue(text, isFramework); 132 if (v != null && !value.equals(v.getValue())) { 133 resValue = v; 134 } else { 135 break; 136 } 137 } else { 138 break; 139 } 140 } 141 } else if (text.startsWith("#") && text.matches("#\\p{XDigit}+")) { //$NON-NLS-1$ 142 resValue = new ResourceValue(ResourceType.COLOR, property.getName(), text, false); 143 } 144 145 if (resValue != null && resValue.getValue() != null) { 146 String value = resValue.getValue(); 147 // Decide whether it's a color, an image, a nine patch etc 148 // and decide how to render it 149 if (value.startsWith("#") || value.endsWith(DOT_XML) //$NON-NLS-1$ 150 && value.contains("res/color")) { //$NON-NLS-1$ // TBD: File.separator? 151 XmlProperty xmlProperty = (XmlProperty) property; 152 ResourceResolver resolver = 153 xmlProperty.getGraphicalEditor().getResourceResolver(); 154 RGB rgb = ResourceHelper.resolveColor(resolver, resValue); 155 if (rgb != null) { 156 Color color = new Color(gc.getDevice(), rgb); 157 // draw color sample 158 Color oldBackground = gc.getBackground(); 159 Color oldForeground = gc.getForeground(); 160 try { 161 int width_c = SAMPLE_SIZE; 162 int height_c = SAMPLE_SIZE; 163 int x_c = x; 164 int y_c = y + (height - height_c) / 2; 165 // update rest bounds 166 int delta = SAMPLE_SIZE + SAMPLE_MARGIN; 167 x += delta; 168 width -= delta; 169 // fill 170 gc.setBackground(color); 171 gc.fillRectangle(x_c, y_c, width_c, height_c); 172 // draw line 173 gc.setForeground(IColorConstants.gray); 174 gc.drawRectangle(x_c, y_c, width_c, height_c); 175 } finally { 176 gc.setBackground(oldBackground); 177 gc.setForeground(oldForeground); 178 } 179 color.dispose(); 180 } 181 } else { 182 Image swtImage = null; 183 if (value.endsWith(DOT_XML) && value.contains("res/drawable")) { // TBD: Filesep? 184 Map<String, Image> cache = getImageCache(property); 185 swtImage = cache.get(value); 186 if (swtImage == null) { 187 XmlProperty xmlProperty = (XmlProperty) property; 188 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); 189 RenderService service = RenderService.create(graphicalEditor); 190 service.setSize(SAMPLE_SIZE, SAMPLE_SIZE); 191 BufferedImage drawable = service.renderDrawable(resValue); 192 if (drawable != null) { 193 swtImage = SwtUtils.convertToSwt(gc.getDevice(), drawable, 194 true /*transferAlpha*/, -1); 195 cache.put(value, swtImage); 196 } 197 } 198 } else if (value.endsWith(DOT_PNG)) { 199 // TODO: 9-patch handling? 200 //if (text.endsWith(DOT_9PNG)) { 201 // // 9-patch image: How do we paint this? 202 // URL url = new File(text).toURI().toURL(); 203 // NinePatch ninepatch = NinePatch.load(url, false /* ?? */); 204 // BufferedImage image = ninepatch.getImage(); 205 //} 206 Map<String, Image> cache = getImageCache(property); 207 swtImage = cache.get(value); 208 if (swtImage == null) { 209 File file = new File(value); 210 if (file.exists()) { 211 try { 212 BufferedImage awtImage = ImageIO.read(file); 213 if (awtImage != null && awtImage.getWidth() > 0 214 && awtImage.getHeight() > 0) { 215 awtImage = ImageUtils.cropBlank(awtImage, null); 216 if (awtImage != null) { 217 // Scale image 218 int imageWidth = awtImage.getWidth(); 219 int imageHeight = awtImage.getHeight(); 220 int maxWidth = 3 * height; 221 222 if (imageWidth > maxWidth || imageHeight > height) { 223 double scale = height / (double) imageHeight; 224 int scaledWidth = (int) (imageWidth * scale); 225 if (scaledWidth > maxWidth) { 226 scale = maxWidth / (double) imageWidth; 227 } 228 awtImage = ImageUtils.scale(awtImage, scale, 229 scale); 230 } 231 swtImage = SwtUtils.convertToSwt(gc.getDevice(), 232 awtImage, true /*transferAlpha*/, -1); 233 } 234 } 235 } catch (IOException e) { 236 AdtPlugin.log(e, value); 237 } 238 } 239 cache.put(value, swtImage); 240 } 241 242 } else if (value != null) { 243 // It's a normal string: if different from the text, paint 244 // it in parentheses, e.g. 245 // @string/foo: Foo Bar (probably cropped) 246 if (!value.equals(text) && !value.equals("@null")) { //$NON-NLS-1$ 247 resolvedText = value; 248 } 249 } 250 251 if (swtImage != null) { 252 // Make a square the size of the height 253 ImageData imageData = swtImage.getImageData(); 254 int imageWidth = imageData.width; 255 int imageHeight = imageData.height; 256 if (imageWidth > 0 && imageHeight > 0) { 257 gc.drawImage(swtImage, x, y + (height - imageHeight) / 2); 258 int delta = imageWidth + SAMPLE_MARGIN; 259 x += delta; 260 width -= delta; 261 } 262 } 263 } 264 } 265 266 DrawUtils.drawStringCV(gc, text, x, y, width, height); 267 268 if (resolvedText != null && resolvedText.length() > 0) { 269 Point size = gc.stringExtent(text); 270 x += size.x; 271 width -= size.x; 272 273 x += SAMPLE_MARGIN; 274 width -= SAMPLE_MARGIN; 275 276 if (width > 0) { 277 Color oldForeground = gc.getForeground(); 278 try { 279 gc.setForeground(PropertyTable.COLOR_PROPERTY_FG_DEFAULT); 280 DrawUtils.drawStringCV(gc, '(' + resolvedText + ')', x, y, width, height); 281 } finally { 282 gc.setForeground(oldForeground); 283 } 284 } 285 } 286 } 287 } 288 289 @Override setEditorText(Property property, String text)290 protected boolean setEditorText(Property property, String text) throws Exception { 291 property.setValue(text); 292 return true; 293 } 294 openDialog(PropertyTable propertyTable, Property property)295 private void openDialog(PropertyTable propertyTable, Property property) throws Exception { 296 XmlProperty xmlProperty = (XmlProperty) property; 297 IAttributeInfo attributeInfo = xmlProperty.getDescriptor().getAttributeInfo(); 298 299 boolean isId = xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID); 300 if (isId) { 301 // When editing the id attribute, don't offer a resource chooser: usually 302 // you want to enter a *new* id here 303 attributeInfo = null; 304 } 305 306 boolean referenceAllowed = false; 307 if (attributeInfo != null) { 308 EnumSet<Format> formats = attributeInfo.getFormats(); 309 ResourceType type = null; 310 List<ResourceType> types = null; 311 if (formats.contains(Format.FLAG)) { 312 FlagXmlPropertyDialog dialog = 313 new FlagXmlPropertyDialog(propertyTable.getShell(), 314 "Select Flag Values", false /* radio */, 315 attributeInfo.getFlagValues(), xmlProperty); 316 317 dialog.open(); 318 return; 319 320 } else if (formats.contains(Format.ENUM)) { 321 FlagXmlPropertyDialog dialog = 322 new FlagXmlPropertyDialog(propertyTable.getShell(), 323 "Select Enum Value", true /* radio */, 324 attributeInfo.getEnumValues(), xmlProperty); 325 dialog.open(); 326 return; 327 } else { 328 for (Format format : formats) { 329 ResourceType t = format.getResourceType(); 330 if (t != null) { 331 if (type != null) { 332 if (types == null) { 333 types = new ArrayList<ResourceType>(); 334 types.add(type); 335 } 336 types.add(t); 337 } 338 type = t; 339 } else if (format == Format.REFERENCE) { 340 referenceAllowed = true; 341 } 342 } 343 } 344 if (types != null || referenceAllowed) { 345 // Multiple resource types (such as string *and* boolean): 346 // just use a reference chooser 347 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); 348 LayoutEditorDelegate delegate = graphicalEditor.getEditorDelegate(); 349 IProject project = delegate.getEditor().getProject(); 350 if (project != null) { 351 // get the resource repository for this project and the system resources. 352 ResourceRepository projectRepository = 353 ResourceManager.getInstance().getProjectResources(project); 354 Shell shell = AdtPlugin.getDisplay().getActiveShell(); 355 ReferenceChooserDialog dlg = new ReferenceChooserDialog( 356 project, 357 projectRepository, 358 shell); 359 dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor)); 360 361 String currentValue = (String) property.getValue(); 362 dlg.setCurrentResource(currentValue); 363 364 if (dlg.open() == Window.OK) { 365 String resource = dlg.getCurrentResource(); 366 if (resource != null) { 367 // Returns null for cancel, "" for clear and otherwise a new value 368 if (resource.length() > 0) { 369 property.setValue(resource); 370 } else { 371 property.setValue(null); 372 } 373 } 374 } 375 376 return; 377 } 378 379 } else if (type != null) { 380 // Single resource type: use a resource chooser 381 GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); 382 String currentValue = (String) property.getValue(); 383 // TODO: Add validator factory? 384 String resource = ResourceChooser.chooseResource(graphicalEditor, 385 type, currentValue, null /* validator */); 386 // Returns null for cancel, "" for clear and otherwise a new value 387 if (resource != null) { 388 if (resource.length() > 0) { 389 property.setValue(resource); 390 } else { 391 property.setValue(null); 392 } 393 } 394 395 return; 396 } 397 } 398 399 // Fallback: Just use a plain string editor 400 StringXmlPropertyDialog dialog = 401 new StringXmlPropertyDialog(propertyTable.getShell(), property); 402 if (dialog.open() == Window.OK) { 403 // TODO: Do I need to activate? 404 } 405 } 406 407 /** Qualified name for the per-project persistent property include-map */ 408 private final static QualifiedName CACHE_NAME = new QualifiedName(AdtPlugin.PLUGIN_ID, 409 "property-images");//$NON-NLS-1$ 410 411 @NonNull getImageCache(@onNull Property property)412 private static Map<String, Image> getImageCache(@NonNull Property property) { 413 XmlProperty xmlProperty = (XmlProperty) property; 414 IProject project = xmlProperty.getGraphicalEditor().getProject(); 415 try { 416 Map<String, Image> cache = (Map<String, Image>) project.getSessionProperty(CACHE_NAME); 417 if (cache == null) { 418 cache = Maps.newHashMap(); 419 project.setSessionProperty(CACHE_NAME, cache); 420 } 421 422 return cache; 423 } catch (CoreException e) { 424 AdtPlugin.log(e, null); 425 return Maps.newHashMap(); 426 } 427 } 428 } 429