/* * Copyright (C) 2012 The Android Open Source Project * * Licensed under the Eclipse Public License, Version 1.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.eclipse.org/org/documents/epl-v10.php * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ide.eclipse.adt.internal.editors.layout.properties; import static com.android.SdkConstants.ANDROID_PREFIX; import static com.android.SdkConstants.ANDROID_THEME_PREFIX; import static com.android.SdkConstants.ATTR_ID; import static com.android.SdkConstants.DOT_PNG; import static com.android.SdkConstants.DOT_XML; import static com.android.SdkConstants.NEW_ID_PREFIX; import static com.android.SdkConstants.PREFIX_RESOURCE_REF; import static com.android.SdkConstants.PREFIX_THEME_REF; import static com.android.ide.common.layout.BaseViewRule.stripIdPrefix; import com.android.annotations.NonNull; import com.android.ide.common.api.IAttributeInfo; import com.android.ide.common.api.IAttributeInfo.Format; import com.android.ide.common.layout.BaseViewRule; import com.android.ide.common.rendering.api.ResourceValue; import com.android.ide.common.resources.ResourceRepository; import com.android.ide.common.resources.ResourceResolver; import com.android.ide.eclipse.adt.AdtPlugin; import com.android.ide.eclipse.adt.internal.editors.common.CommonXmlEditor; import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.GraphicalEditorPart; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.LayoutCanvas; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.RenderService; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SelectionManager; import com.android.ide.eclipse.adt.internal.editors.layout.gle2.SwtUtils; import com.android.ide.eclipse.adt.internal.editors.layout.gre.NodeProxy; import com.android.ide.eclipse.adt.internal.preferences.AdtPrefs; import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResourceWizard; import com.android.ide.eclipse.adt.internal.refactorings.core.RenameResult; import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; import com.android.ide.eclipse.adt.internal.resources.manager.ResourceManager; import com.android.ide.eclipse.adt.internal.ui.ReferenceChooserDialog; import com.android.ide.eclipse.adt.internal.ui.ResourceChooser; import com.android.ide.eclipse.adt.internal.ui.ResourcePreviewHelper; import com.android.resources.ResourceType; import com.google.common.collect.Maps; import org.eclipse.core.resources.IProject; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.QualifiedName; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.MessageDialogWithToggle; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.window.Window; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.RGB; import org.eclipse.swt.widgets.Shell; import org.eclipse.wb.draw2d.IColorConstants; import org.eclipse.wb.internal.core.model.property.Property; import org.eclipse.wb.internal.core.model.property.editor.AbstractTextPropertyEditor; import org.eclipse.wb.internal.core.model.property.editor.presentation.ButtonPropertyEditorPresentation; import org.eclipse.wb.internal.core.model.property.editor.presentation.PropertyEditorPresentation; import org.eclipse.wb.internal.core.model.property.table.PropertyTable; import org.eclipse.wb.internal.core.utils.ui.DrawUtils; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.EnumSet; import java.util.List; import java.util.Map; import javax.imageio.ImageIO; /** * Special property editor used for the {@link XmlProperty} instances which handles * editing the XML properties, rendering defaults by looking up the actual colors and images, */ class XmlPropertyEditor extends AbstractTextPropertyEditor { public static final XmlPropertyEditor INSTANCE = new XmlPropertyEditor(); private static final int SAMPLE_SIZE = 10; private static final int SAMPLE_MARGIN = 3; protected XmlPropertyEditor() { } private final PropertyEditorPresentation mPresentation = new ButtonPropertyEditorPresentation() { @Override protected void onClick(PropertyTable propertyTable, Property property) throws Exception { openDialog(propertyTable, property); } }; @Override public PropertyEditorPresentation getPresentation() { return mPresentation; } @Override public String getText(Property property) throws Exception { Object value = property.getValue(); if (value instanceof String) { return (String) value; } return null; } @Override protected String getEditorText(Property property) throws Exception { return getText(property); } @Override public void paint(Property property, GC gc, int x, int y, int width, int height) throws Exception { String text = getText(property); if (text != null) { ResourceValue resValue = null; String resolvedText = null; // TODO: Use the constants for @, ?, @android: etc if (text.startsWith("@") || text.startsWith("?")) { //$NON-NLS-1$ //$NON-NLS-2$ // Yes, try to resolve it in order to show better info XmlProperty xmlProperty = (XmlProperty) property; GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); if (graphicalEditor != null) { ResourceResolver resolver = graphicalEditor.getResourceResolver(); boolean isFramework = text.startsWith(ANDROID_PREFIX) || text.startsWith(ANDROID_THEME_PREFIX); resValue = resolver.findResValue(text, isFramework); while (resValue != null && resValue.getValue() != null) { String value = resValue.getValue(); if (value.startsWith(PREFIX_RESOURCE_REF) || value.startsWith(PREFIX_THEME_REF)) { // TODO: do I have to strip off the @ too? isFramework = isFramework || value.startsWith(ANDROID_PREFIX) || value.startsWith(ANDROID_THEME_PREFIX); ResourceValue v = resolver.findResValue(text, isFramework); if (v != null && !value.equals(v.getValue())) { resValue = v; } else { break; } } else { break; } } } } else if (text.startsWith("#") && text.matches("#\\p{XDigit}+")) { //$NON-NLS-1$ resValue = new ResourceValue(ResourceType.COLOR, property.getName(), text, false); } if (resValue != null && resValue.getValue() != null) { String value = resValue.getValue(); // Decide whether it's a color, an image, a nine patch etc // and decide how to render it if (value.startsWith("#") || value.endsWith(DOT_XML) //$NON-NLS-1$ && value.contains("res/color")) { //$NON-NLS-1$ // TBD: File.separator? XmlProperty xmlProperty = (XmlProperty) property; GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); if (graphicalEditor != null) { ResourceResolver resolver = graphicalEditor.getResourceResolver(); RGB rgb = ResourceHelper.resolveColor(resolver, resValue); if (rgb != null) { Color color = new Color(gc.getDevice(), rgb); // draw color sample Color oldBackground = gc.getBackground(); Color oldForeground = gc.getForeground(); try { int width_c = SAMPLE_SIZE; int height_c = SAMPLE_SIZE; int x_c = x; int y_c = y + (height - height_c) / 2; // update rest bounds int delta = SAMPLE_SIZE + SAMPLE_MARGIN; x += delta; width -= delta; // fill gc.setBackground(color); gc.fillRectangle(x_c, y_c, width_c, height_c); // draw line gc.setForeground(IColorConstants.gray); gc.drawRectangle(x_c, y_c, width_c, height_c); } finally { gc.setBackground(oldBackground); gc.setForeground(oldForeground); } color.dispose(); } } } else { Image swtImage = null; if (value.endsWith(DOT_XML) && value.contains("res/drawable")) { // TBD: Filesep? Map<String, Image> cache = getImageCache(property); swtImage = cache.get(value); if (swtImage == null) { XmlProperty xmlProperty = (XmlProperty) property; GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); RenderService service = RenderService.create(graphicalEditor); service.setOverrideRenderSize(SAMPLE_SIZE, SAMPLE_SIZE); BufferedImage drawable = service.renderDrawable(resValue); if (drawable != null) { swtImage = SwtUtils.convertToSwt(gc.getDevice(), drawable, true /*transferAlpha*/, -1); cache.put(value, swtImage); } } } else if (value.endsWith(DOT_PNG)) { // TODO: 9-patch handling? //if (text.endsWith(DOT_9PNG)) { // // 9-patch image: How do we paint this? // URL url = new File(text).toURI().toURL(); // NinePatch ninepatch = NinePatch.load(url, false /* ?? */); // BufferedImage image = ninepatch.getImage(); //} Map<String, Image> cache = getImageCache(property); swtImage = cache.get(value); if (swtImage == null) { File file = new File(value); if (file.exists()) { try { BufferedImage awtImage = ImageIO.read(file); if (awtImage != null && awtImage.getWidth() > 0 && awtImage.getHeight() > 0) { awtImage = ImageUtils.cropBlank(awtImage, null); if (awtImage != null) { // Scale image int imageWidth = awtImage.getWidth(); int imageHeight = awtImage.getHeight(); int maxWidth = 3 * height; if (imageWidth > maxWidth || imageHeight > height) { double scale = height / (double) imageHeight; int scaledWidth = (int) (imageWidth * scale); if (scaledWidth > maxWidth) { scale = maxWidth / (double) imageWidth; } awtImage = ImageUtils.scale(awtImage, scale, scale); } swtImage = SwtUtils.convertToSwt(gc.getDevice(), awtImage, true /*transferAlpha*/, -1); } } } catch (IOException e) { AdtPlugin.log(e, value); } } cache.put(value, swtImage); } } else if (value != null) { // It's a normal string: if different from the text, paint // it in parentheses, e.g. // @string/foo: Foo Bar (probably cropped) if (!value.equals(text) && !value.equals("@null")) { //$NON-NLS-1$ resolvedText = value; } } if (swtImage != null) { // Make a square the size of the height ImageData imageData = swtImage.getImageData(); int imageWidth = imageData.width; int imageHeight = imageData.height; if (imageWidth > 0 && imageHeight > 0) { gc.drawImage(swtImage, x, y + (height - imageHeight) / 2); int delta = imageWidth + SAMPLE_MARGIN; x += delta; width -= delta; } } } } DrawUtils.drawStringCV(gc, text, x, y, width, height); if (resolvedText != null && resolvedText.length() > 0) { Point size = gc.stringExtent(text); x += size.x; width -= size.x; x += SAMPLE_MARGIN; width -= SAMPLE_MARGIN; if (width > 0) { Color oldForeground = gc.getForeground(); try { gc.setForeground(PropertyTable.COLOR_PROPERTY_FG_DEFAULT); DrawUtils.drawStringCV(gc, '(' + resolvedText + ')', x, y, width, height); } finally { gc.setForeground(oldForeground); } } } } } @Override protected boolean setEditorText(Property property, String text) throws Exception { Object oldValue = property.getValue(); String old = oldValue != null ? oldValue.toString() : null; // If users enters a new id without specifying the @id/@+id prefix, insert it boolean isId = isIdProperty(property); if (isId && !text.startsWith(PREFIX_RESOURCE_REF)) { text = NEW_ID_PREFIX + text; } // Handle id refactoring: if you change an id, may want to update references too. // Ask user. if (isId && property instanceof XmlProperty && old != null && !old.isEmpty() && text != null && !text.isEmpty() && !text.equals(old)) { XmlProperty xmlProperty = (XmlProperty) property; IPreferenceStore store = AdtPlugin.getDefault().getPreferenceStore(); String refactorPref = store.getString(AdtPrefs.PREFS_REFACTOR_IDS); boolean performRefactor = false; Shell shell = AdtPlugin.getShell(); if (refactorPref == null || refactorPref.isEmpty() || refactorPref.equals(MessageDialogWithToggle.PROMPT)) { MessageDialogWithToggle dialog = MessageDialogWithToggle.openYesNoCancelQuestion( shell, "Update References?", "Update all references as well? " + "This will update all XML references and Java R field references.", "Do not show again", false, store, AdtPrefs.PREFS_REFACTOR_IDS); switch (dialog.getReturnCode()) { case IDialogConstants.CANCEL_ID: return false; case IDialogConstants.YES_ID: performRefactor = true; break; case IDialogConstants.NO_ID: performRefactor = false; break; } } else { performRefactor = refactorPref.equals(MessageDialogWithToggle.ALWAYS); } if (performRefactor) { CommonXmlEditor xmlEditor = xmlProperty.getXmlEditor(); if (xmlEditor != null) { IProject project = xmlEditor.getProject(); if (project != null && shell != null) { RenameResourceWizard.renameResource(shell, project, ResourceType.ID, stripIdPrefix(old), stripIdPrefix(text), false); } } } } property.setValue(text); return true; } private static boolean isIdProperty(Property property) { XmlProperty xmlProperty = (XmlProperty) property; return xmlProperty.getDescriptor().getXmlLocalName().equals(ATTR_ID); } private void openDialog(PropertyTable propertyTable, Property property) throws Exception { XmlProperty xmlProperty = (XmlProperty) property; IAttributeInfo attributeInfo = xmlProperty.getDescriptor().getAttributeInfo(); if (isIdProperty(property)) { Object value = xmlProperty.getValue(); if (value != null && !value.toString().isEmpty()) { GraphicalEditorPart editor = xmlProperty.getGraphicalEditor(); if (editor != null) { LayoutCanvas canvas = editor.getCanvasControl(); SelectionManager manager = canvas.getSelectionManager(); NodeProxy primary = canvas.getNodeFactory().create(xmlProperty.getNode()); if (primary != null) { RenameResult result = manager.performRename(primary, null); if (result.isCanceled()) { return; } else if (!result.isUnavailable()) { String name = result.getName(); String id = NEW_ID_PREFIX + BaseViewRule.stripIdPrefix(name); xmlProperty.setValue(id); return; } } } } // When editing the id attribute, don't offer a resource chooser: usually // you want to enter a *new* id here attributeInfo = null; } boolean referenceAllowed = false; if (attributeInfo != null) { EnumSet<Format> formats = attributeInfo.getFormats(); ResourceType type = null; List<ResourceType> types = null; if (formats.contains(Format.FLAG)) { String[] flagValues = attributeInfo.getFlagValues(); if (flagValues != null) { FlagXmlPropertyDialog dialog = new FlagXmlPropertyDialog(propertyTable.getShell(), "Select Flag Values", false /* radio */, flagValues, xmlProperty); dialog.open(); return; } } else if (formats.contains(Format.ENUM)) { String[] enumValues = attributeInfo.getEnumValues(); if (enumValues != null) { FlagXmlPropertyDialog dialog = new FlagXmlPropertyDialog(propertyTable.getShell(), "Select Enum Value", true /* radio */, enumValues, xmlProperty); dialog.open(); return; } } else { for (Format format : formats) { ResourceType t = format.getResourceType(); if (t != null) { if (type != null) { if (types == null) { types = new ArrayList<ResourceType>(); types.add(type); } types.add(t); } type = t; } else if (format == Format.REFERENCE) { referenceAllowed = true; } } } if (types != null || referenceAllowed) { // Multiple resource types (such as string *and* boolean): // just use a reference chooser GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); if (graphicalEditor != null) { LayoutEditorDelegate delegate = graphicalEditor.getEditorDelegate(); IProject project = delegate.getEditor().getProject(); if (project != null) { // get the resource repository for this project and the system resources. ResourceRepository projectRepository = ResourceManager.getInstance().getProjectResources(project); Shell shell = AdtPlugin.getShell(); ReferenceChooserDialog dlg = new ReferenceChooserDialog( project, projectRepository, shell); dlg.setPreviewHelper(new ResourcePreviewHelper(dlg, graphicalEditor)); String currentValue = (String) property.getValue(); dlg.setCurrentResource(currentValue); if (dlg.open() == Window.OK) { String resource = dlg.getCurrentResource(); if (resource != null) { // Returns null for cancel, "" for clear and otherwise a new value if (resource.length() > 0) { property.setValue(resource); } else { property.setValue(null); } } } return; } } } else if (type != null) { // Single resource type: use a resource chooser GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); if (graphicalEditor != null) { String currentValue = (String) property.getValue(); // TODO: Add validator factory? String resource = ResourceChooser.chooseResource(graphicalEditor, type, currentValue, null /* validator */); // Returns null for cancel, "" for clear and otherwise a new value if (resource != null) { if (resource.length() > 0) { property.setValue(resource); } else { property.setValue(null); } } } return; } } // Fallback: Just use a plain string editor StringXmlPropertyDialog dialog = new StringXmlPropertyDialog(propertyTable.getShell(), property); if (dialog.open() == Window.OK) { // TODO: Do I need to activate? } } /** Qualified name for the per-project persistent property include-map */ private final static QualifiedName CACHE_NAME = new QualifiedName(AdtPlugin.PLUGIN_ID, "property-images");//$NON-NLS-1$ @NonNull private static Map<String, Image> getImageCache(@NonNull Property property) { XmlProperty xmlProperty = (XmlProperty) property; GraphicalEditorPart graphicalEditor = xmlProperty.getGraphicalEditor(); IProject project = graphicalEditor.getProject(); try { Map<String, Image> cache = (Map<String, Image>) project.getSessionProperty(CACHE_NAME); if (cache == null) { cache = Maps.newHashMap(); project.setSessionProperty(CACHE_NAME, cache); } return cache; } catch (CoreException e) { AdtPlugin.log(e, null); return Maps.newHashMap(); } } }