blob: 48efc237212d57a5e65400b24e4337c0fadbdd24 [file] [log] [blame]
/*
* Copyright (C) 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
*
* 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.tools.idea.editors.theme;
import com.android.SdkConstants;
import com.android.ide.common.rendering.api.RenderResources;
import com.android.ide.common.rendering.api.ResourceValue;
import com.android.ide.common.resources.ResourceResolver;
import com.android.ide.common.resources.ResourceUrl;
import com.android.resources.ResourceFolderType;
import com.android.resources.ResourceType;
import com.android.sdklib.IAndroidTarget;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.editors.theme.attributes.editors.ColorRendererEditor;
import com.android.tools.idea.editors.theme.attributes.editors.DrawableRendererEditor;
import com.android.tools.idea.editors.theme.ui.ResourceComponent;
import com.android.tools.idea.rendering.RenderTask;
import com.android.tools.idea.rendering.ResourceHelper;
import com.android.tools.swing.ui.SwatchComponent;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.ui.JBPopupMenu;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.xml.XmlTag;
import com.intellij.ui.JBColor;
import org.jetbrains.android.dom.AndroidDomElement;
import org.jetbrains.android.dom.color.ColorSelector;
import org.jetbrains.android.dom.drawable.DrawableSelector;
import org.jetbrains.android.facet.AndroidFacet;
import org.jetbrains.android.sdk.AndroidTargetData;
import org.jetbrains.android.uipreview.ChooseResourceDialog;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.android.util.AndroidUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class StateListPicker extends JPanel {
private static final String LABEL_TEMPLATE = "<html><nobr><b><font color=\"#%1$s\">%2$s</font></b>";
private static final ResourceType[] DIMENSIONS_ONLY = {ResourceType.DIMEN};
private final Module myModule;
private final Configuration myConfiguration;
private final ResourceHelper.StateList myStateList;
private final List<StateComponent> myStateComponents;
private @Nullable final RenderTask myRenderTask;
public StateListPicker(@NotNull ResourceHelper.StateList stateList, @NotNull Module module, @NotNull Configuration configuration) {
myStateList = stateList;
myModule = module;
myConfiguration = configuration;
myStateComponents = Lists.newArrayListWithCapacity(stateList.getStates().size());
myRenderTask = DrawableRendererEditor.configureRenderTask(module, configuration);
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
for (ResourceHelper.StateListState state : myStateList.getStates()) {
StateComponent stateComponent = createStateComponent(state);
stateComponent.addValueActionListener(new ValueActionListener(state, stateComponent));
stateComponent.addAlphaActionListener(new AlphaActionListener(state, stateComponent));
add(stateComponent);
}
}
@NotNull
private StateComponent createStateComponent(@NotNull ResourceHelper.StateListState state) {
final StateComponent stateComponent = new StateComponent();
myStateComponents.add(stateComponent);
String stateValue = state.getValue();
updateComponent(stateComponent, stateValue, state.getAlpha());
Map<String, Boolean> attributes = state.getAttributes();
List<String> attributeDescriptions = new ArrayList<String>();
for (Map.Entry<String, Boolean> attribute : attributes.entrySet()) {
String description = attribute.getKey().substring(ResourceHelper.STATE_NAME_PREFIX.length());
if (!attribute.getValue()) {
description = "Not " + description;
}
attributeDescriptions.add(StringUtil.capitalize(description));
}
String stateDescription = attributeDescriptions.size() == 0 ? "Default" : Joiner.on(", ").join(attributeDescriptions);
stateComponent.setNameText(String.format(LABEL_TEMPLATE, ThemeEditorConstants.RESOURCE_ITEM_COLOR.toString(), stateDescription));
stateComponent.setComponentPopupMenu(createAlphaPopupMenu(state, stateComponent));
return stateComponent;
}
@NotNull
private JBPopupMenu createAlphaPopupMenu(@NotNull final ResourceHelper.StateListState state,
@NotNull final StateComponent stateComponent) {
JBPopupMenu popupMenu = new JBPopupMenu();
final JMenuItem deleteAlpha = new JMenuItem("Delete alpha");
popupMenu.add(deleteAlpha);
deleteAlpha.setVisible(state.getAlpha() != null);
final JMenuItem createAlpha = new JMenuItem("Create alpha");
popupMenu.add(createAlpha);
createAlpha.setVisible(state.getAlpha() == null);
deleteAlpha.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
stateComponent.getAlphaComponent().setVisible(false);
state.setAlpha(null);
updateComponent(stateComponent, state.getValue(), state.getAlpha());
deleteAlpha.setVisible(false);
createAlpha.setVisible(true);
}
});
createAlpha.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
AlphaActionListener listener = stateComponent.getAlphaActionListener();
if (listener == null) {
return;
}
listener.actionPerformed(new ActionEvent(stateComponent.getAlphaComponent(), ActionEvent.ACTION_PERFORMED, null));
if (state.getAlpha() != null) {
stateComponent.getAlphaComponent().setVisible(true);
createAlpha.setVisible(false);
deleteAlpha.setVisible(true);
}
}
});
return popupMenu;
}
public void updateStateList(@NotNull List<VirtualFile> files) {
Project project = myModule.getProject();
if (!AndroidResourceUtil.ensureFilesWritable(project, files)) {
return;
}
List<PsiFile> psiFiles = Lists.newArrayListWithCapacity(files.size());
PsiManager manager = PsiManager.getInstance(project);
for (VirtualFile file : files) {
PsiFile psiFile = manager.findFile(file);
if (psiFile != null) {
psiFiles.add(psiFile);
}
}
final List<AndroidDomElement> selectors = Lists.newArrayListWithCapacity(files.size());
Class<? extends AndroidDomElement> selectorClass;
if (myStateList.getType() == ResourceFolderType.COLOR) {
selectorClass = ColorSelector.class;
}
else {
selectorClass = DrawableSelector.class;
}
for (VirtualFile file : files) {
final AndroidDomElement selector = AndroidUtils.loadDomElement(myModule, file, selectorClass);
if (selector == null) {
AndroidUtils.reportError(project, file.getName() + " is not a statelist file");
return;
}
selectors.add(selector);
}
new WriteCommandAction.Simple(project, "Change State List", psiFiles.toArray(new PsiFile[psiFiles.size()])) {
@Override
protected void run() {
for (AndroidDomElement selector : selectors) {
XmlTag tag = selector.getXmlTag();
for (XmlTag subtag : tag.getSubTags()) {
subtag.delete();
}
for (ResourceHelper.StateListState state : myStateList.getStates()) {
XmlTag child = tag.createChildTag(SdkConstants.TAG_ITEM, tag.getNamespace(), null, false);
child = tag.addSubTag(child, false);
Map<String, Boolean> attributes = state.getAttributes();
for (String attributeName : attributes.keySet()) {
child.setAttribute(attributeName, SdkConstants.ANDROID_URI, attributes.get(attributeName).toString());
}
if (state.getAlpha() != null) {
child.setAttribute("alpha", SdkConstants.ANDROID_URI, state.getAlpha());
}
if (selector instanceof ColorSelector) {
child.setAttribute(SdkConstants.ATTR_COLOR, SdkConstants.ANDROID_URI, state.getValue());
}
else if (selector instanceof DrawableSelector) {
child.setAttribute(SdkConstants.ATTR_DRAWABLE, SdkConstants.ANDROID_URI, state.getValue());
}
}
}
}
}.execute();
}
/**
* Returns a {@Link ValidationInfo} specifying which of the state list component has a value which is a private resource.
* If there is no such component, returns null.
*/
@Nullable
public ValidationInfo getPrivateResourceError() {
IAndroidTarget target = myConfiguration.getTarget();
assert target != null;
final AndroidTargetData androidTargetData = AndroidTargetData.getTargetData(target, myModule);
assert androidTargetData != null;
ValidationInfo error = null;
String errorText = "%s is a private Android resource";
String resourceValue;
for (StateComponent component : myStateComponents) {
resourceValue = component.getResourceValue();
if (isResourcePrivate(resourceValue, androidTargetData)) {
error = component.getResourceComponent().createSwatchValidationInfo(String.format(errorText, resourceValue));
break;
}
else {
resourceValue = component.getAlphaValue();
if (isResourcePrivate(resourceValue, androidTargetData)) {
error = new ValidationInfo(String.format(errorText, resourceValue), component.getAlphaComponent());
break;
}
}
}
return error;
}
private static boolean isResourcePrivate(@NotNull String resourceValue, @NotNull AndroidTargetData targetData) {
ResourceUrl url = ResourceUrl.parse(resourceValue);
return url != null && url.framework && !targetData.isResourcePublic(url.type.getName(), url.name);
}
class ValueActionListener implements ActionListener {
private final ResourceHelper.StateListState myState;
private final StateComponent myComponent;
public ValueActionListener(ResourceHelper.StateListState state, StateComponent stateComponent) {
myState = state;
myComponent = stateComponent;
}
@Override
public void actionPerformed(ActionEvent e) {
ResourceComponent resourceComponent = myComponent.getResourceComponent();
String itemValue = resourceComponent.getValueText();
final String resourceName;
// If it points to an existing resource.
if (!RenderResources.REFERENCE_EMPTY.equals(itemValue) &&
!RenderResources.REFERENCE_NULL.equals(itemValue) &&
itemValue.startsWith(SdkConstants.PREFIX_RESOURCE_REF)) {
// Use the name of that resource.
resourceName = itemValue.substring(itemValue.indexOf('/') + 1);
}
else {
// Otherwise use the name of the attribute.
resourceName = itemValue;
}
ResourceResolver resourceResolver = myConfiguration.getResourceResolver();
assert resourceResolver != null;
String resolvedResource = itemValue;
ResourceValue resValue = resourceResolver.findResValue(itemValue, false);
if (resValue != null) {
if (resValue.getResourceType() == ResourceType.COLOR) {
resolvedResource = ResourceHelper.colorToString(ResourceHelper.resolveColor(resourceResolver, resValue, myModule.getProject()));
}
else {
resolvedResource = resourceResolver.resolveResValue(resValue).getName();
}
}
ResourceType[] allowedTypes;
if (myStateList.getType() == ResourceFolderType.COLOR) {
allowedTypes = ColorRendererEditor.COLORS_ONLY;
}
else {
allowedTypes = ColorRendererEditor.DRAWABLES_ONLY;
}
final ChooseResourceDialog dialog =
new ChooseResourceDialog(myModule, allowedTypes, resolvedResource, null, ChooseResourceDialog.ResourceNameVisibility.FORCE,
resourceName);
dialog.show();
if (dialog.isOK()) {
myState.setValue(dialog.getResourceName());
AndroidFacet facet = AndroidFacet.getInstance(myModule);
if (facet != null) {
facet.refreshResources();
}
updateComponent(myComponent, myState.getValue(), myState.getAlpha());
myComponent.repaint();
}
}
}
private class AlphaActionListener implements ActionListener {
private final ResourceHelper.StateListState myState;
private final StateComponent myComponent;
public AlphaActionListener(ResourceHelper.StateListState state, StateComponent stateComponent) {
myState = state;
myComponent = stateComponent;
}
@Override
public void actionPerformed(ActionEvent e) {
SwatchComponent source = myComponent.getAlphaComponent();
String itemValue = source.getText();
ResourceResolver resourceResolver = myConfiguration.getResourceResolver();
assert resourceResolver != null;
ResourceValue resValue = resourceResolver.findResValue(itemValue, false);
String resolvedResource = resValue != null ? resourceResolver.resolveResValue(resValue).getName() : itemValue;
final ChooseResourceDialog dialog = new ChooseResourceDialog(myModule, DIMENSIONS_ONLY, resolvedResource, null);
dialog.show();
if (dialog.isOK()) {
myState.setAlpha(dialog.getResourceName());
AndroidFacet facet = AndroidFacet.getInstance(myModule);
assert facet != null;
facet.refreshResources();
updateComponent(myComponent, myState.getValue(), myState.getAlpha());
myComponent.repaint();
}
}
}
private void updateComponent(@NotNull StateComponent component, @NotNull String resourceName, @Nullable String alphaValue) {
component.setValueText(resourceName);
component.setAlphaVisible(alphaValue != null);
ResourceResolver resourceResolver = myConfiguration.getResourceResolver();
assert resourceResolver != null;
ResourceValue resValue = resourceResolver.findResValue(resourceName, false);
String value = resValue != null ? resourceResolver.resolveResValue(resValue).getValue() : resourceName;
if (resValue != null && resValue.getResourceType() != ResourceType.COLOR && myRenderTask != null) {
component.setValueIcons(SwatchComponent.imageListOf(myRenderTask.renderDrawableAllStates(resValue)));
}
else {
Color color = ResourceHelper.parseColor(value);
assert color != null;
List<Color> colorList = ImmutableList.of(color);
component.setValueIcons(SwatchComponent.colorListOf(colorList));
if (alphaValue != null) {
try {
float alpha = Float.parseFloat(ResourceHelper.resolveStringValue(resourceResolver, alphaValue));
component.getAlphaComponent().setText(alphaValue);
List<NumericalIcon> list = ImmutableList.of(new NumericalIcon(alpha, getFont()));
component.getAlphaComponent().setSwatchIcons(list);
}
catch (NumberFormatException e) {
AndroidUtils.reportError(myModule.getProject(), String
.format("The alpha attribute in %s/%s does not resolve to a floating point number", myStateList.getDirName(),
myStateList.getFileName()));
}
}
}
}
private class StateComponent extends Box {
private final ResourceComponent myResourceComponent;
private final SwatchComponent myAlphaComponent;
private AlphaActionListener myAlphaActionListener;
public StateComponent() {
super(BoxLayout.PAGE_AXIS);
setFont(StateListPicker.this.getFont());
myResourceComponent = new ResourceComponent();
add(myResourceComponent);
myResourceComponent
.setMaximumSize(new Dimension(myResourceComponent.getMaximumSize().width, myResourceComponent.getPreferredSize().height));
myResourceComponent.setVariantComboVisible(false);
myAlphaComponent = new SwatchComponent((short)1);
myAlphaComponent.setBackground(JBColor.WHITE);
myAlphaComponent.setForeground(null);
add(myAlphaComponent);
myAlphaComponent.setMaximumSize(new Dimension(myAlphaComponent.getMaximumSize().width, myAlphaComponent.getPreferredSize().height));
}
@NotNull
public ResourceComponent getResourceComponent() {
return myResourceComponent;
}
@NotNull
public SwatchComponent getAlphaComponent() {
return myAlphaComponent;
}
public void setNameText(@NotNull String name) {
myResourceComponent.setNameText(name);
}
public void setValueText(@NotNull String value) {
myResourceComponent.setValueText(value);
}
public void setAlphaVisible(boolean isVisible) {
myAlphaComponent.setVisible(isVisible);
}
public void setValueIcons(List<SwatchComponent.SwatchIcon> icons) {
myResourceComponent.setSwatchIcons(icons);
}
@NotNull
public String getResourceValue() {
return myResourceComponent.getValueText();
}
@NotNull
public String getAlphaValue() {
return myAlphaComponent.getText();
}
public void addValueActionListener(@NotNull ValueActionListener listener) {
myResourceComponent.addActionListener(listener);
}
public void addAlphaActionListener(@NotNull AlphaActionListener listener) {
myAlphaComponent.addActionListener(listener);
myAlphaActionListener = listener;
}
@Nullable
public AlphaActionListener getAlphaActionListener() {
return myAlphaActionListener;
}
@Override
public void setComponentPopupMenu(JPopupMenu popup) {
super.setComponentPopupMenu(popup);
myResourceComponent.setComponentPopupMenu(popup);
myAlphaComponent.setComponentPopupMenu(popup);
}
}
private static class NumericalIcon implements SwatchComponent.SwatchIcon {
private final Font myAlphaFont;
private final String myString;
private final Font myStateListFont;
public NumericalIcon(float f, @NotNull Font stateListFont) {
myString = String.format("%.2f", f);
myStateListFont = stateListFont;
myAlphaFont = new Font(myStateListFont.getName(), Font.BOLD, myStateListFont.getSize() - 2);
}
@Override
public void paint(@Nullable Component c, @NotNull Graphics g, int x, int y, int w, int h) {
g.setColor(JBColor.LIGHT_GRAY);
g.fillRect(x, y, w, h);
g.setColor(JBColor.DARK_GRAY);
g.setFont(myAlphaFont);
FontMetrics fm = g.getFontMetrics();
int horizontalMargin = (w + 1 - fm.stringWidth(myString)) / 2;
int verticalMargin = (h + 3 - fm.getAscent()) / 2;
g.drawString(myString, x + horizontalMargin, y + h - verticalMargin);
g.setFont(myStateListFont);
}
}
}