blob: 87fb0e6ed001f04343b7eb867536813439930804 [file] [log] [blame]
/*
* 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();
}
}
}