blob: 147ba4ac6b5f5d054a8bd173873d1c7d344b832f [file] [log] [blame]
/*
* Copyright (C) 2014 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.datamodels;
import com.android.SdkConstants;
import com.android.ide.common.rendering.api.ItemResourceValue;
import com.android.ide.common.rendering.api.StyleResourceValue;
import com.android.ide.common.res2.ResourceItem;
import com.android.resources.ResourceType;
import com.android.tools.idea.configurations.Configuration;
import com.android.tools.idea.editors.theme.StyleResolver;
import com.android.tools.idea.rendering.AppResourceRepository;
import com.android.tools.idea.rendering.LocalResourceRepository;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.command.WriteCommandAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiElementVisitor;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.android.dom.wrappers.ValueResourceElementWrapper;
import org.jetbrains.android.util.AndroidResourceUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* Wrapper for style configurations that allows modifying attributes directly in the XML file.
*/
public class ThemeEditorStyle {
private static final Logger LOG = Logger.getInstance(ThemeEditorStyle.class);
private final StyleResolver myThemeResolver;
private final boolean myIsProjectStyle;
private final boolean myIsFrameworkStyle;
private final String myStyleName;
private final Configuration myConfiguration;
private final Project myProject;
public ThemeEditorStyle(@NotNull StyleResolver resolver,
@NotNull Configuration configuration,
@NotNull String styleName,
boolean isFrameworkStyle) {
myIsFrameworkStyle = isFrameworkStyle;
myThemeResolver = resolver;
myConfiguration = configuration;
myStyleName = styleName;
myProject = configuration.getModule().getProject();
if (!myIsFrameworkStyle) {
//TODO: Do it Better
XmlTag sourceXml = getSourceXmls().get(0);
/*
* Find if the file is contained in the resources folder. If the source file is not contained in the source folder this might be
* coming from a library that we can not actually edit.
*/
VirtualFile parent = sourceXml.getContainingFile() != null && sourceXml.getContainingFile().getVirtualFile() != null ?
sourceXml.getContainingFile().getVirtualFile().getParent() :
null;
myIsProjectStyle =
parent != null && parent.getParent() != null && AndroidResourceUtil.isLocalResourceDirectory(parent.getParent(), myProject);
} else {
myIsProjectStyle = false;
}
}
private StyleResourceValue getStyleResourceValue() {
return myConfiguration.getResourceResolver().getStyle(myStyleName, myIsFrameworkStyle);
}
private List<XmlTag> getSourceXmls() {
assert !myIsFrameworkStyle;
LocalResourceRepository repository = AppResourceRepository.getAppResources(myConfiguration.getModule(), true);
assert repository != null;
List<ResourceItem> resources = repository.getResourceItem(ResourceType.STYLE, myStyleName);
List<XmlTag> xmlTags = new ArrayList<XmlTag>();
for (ResourceItem resource : resources) {
XmlTag tag = LocalResourceRepository.getItemTag(myProject, resource);
assert tag != null;
xmlTags.add(tag);
}
return xmlTags;
}
/**
* Returns whether this is a project style and therefore editable. If the style is not part of the project, it can be part or the framework
* or a library.
*/
public boolean isProjectStyle() {
return myIsProjectStyle;
}
/**
* Returns whether this style is editable.
*/
public boolean isReadOnly() {
return !isProjectStyle();
}
/**
* Returns the style name. If this is a framework style, it will include the "android:" prefix.
*/
@NotNull
public String getName() {
return StyleResolver.getQualifiedStyleName(getStyleResourceValue());
}
/**
* Returns the style name without namespaces or prefixes.
*/
@NotNull
public String getSimpleName() {
return getStyleResourceValue().getName();
}
/**
* Returns the theme values.
*/
@NotNull
public Collection<ItemResourceValue> getValues() {
return getStyleResourceValue().getValues();
}
/**
* Returns the style parent or null if this is a root style.
*/
@Nullable
public ThemeEditorStyle getParent() {
StyleResourceValue parent = myConfiguration.getResourceResolver().getParent(getStyleResourceValue());
if (parent == null) {
return null;
}
return myThemeResolver.getStyle(StyleResolver.getQualifiedStyleName(parent));
}
/**
* Returns the XmlTag that contains the value for a given attribute in the current style.
* @param attribute The style attribute name.
* @return The {@link XmlTag} or null if the attribute does not exist in this theme.
*/
@Nullable
private XmlTag getValueTag(@NotNull XmlTag sourceTag, @NotNull final String attribute) {
if (!isProjectStyle()) {
// Non project styles do not contain local values.
return null;
}
final Ref<XmlTag> resultXmlTag = new Ref<XmlTag>();
ApplicationManager.getApplication().assertReadAccessAllowed();
sourceTag.acceptChildren(new PsiElementVisitor() {
@Override
public void visitElement(PsiElement element) {
super.visitElement(element);
if (!(element instanceof XmlTag)) {
return;
}
final XmlTag tag = (XmlTag) element;
if (SdkConstants.TAG_ITEM.equals(tag.getName()) && attribute.equals(tag.getAttributeValue(SdkConstants.ATTR_NAME))) {
resultXmlTag.set(tag);
}
}
});
return resultXmlTag.get();
}
/**
* Returns whether an attribute is locally defined by this style or not.
* @param attribute The style attribute name.
*/
public boolean isAttributeDefined(@NotNull String attribute) {
return getStyleResourceValue().getNames().contains(attribute);
}
@Nullable
public String getValue(@NotNull final String attribute) {
throw new UnsupportedOperationException();
}
/**
* Sets the attribute value and returns whether the value was created or just modified.
* @param attribute The style attribute name.
* @param value The attribute value.
*/
public boolean setValue(@NotNull final String attribute, @NotNull final String value) {
if (!isProjectStyle()) {
throw new UnsupportedOperationException("Non project styles can not be modified");
}
for (final XmlTag sourceXml : getSourceXmls()) {
// TODO: Check if the current value is defined by one of the parents and remove the attribute.
final XmlTag tag = getValueTag(sourceXml, attribute);
if (tag != null) {
// Update the value.
new WriteCommandAction.Simple(myProject, "Setting value of " + tag.getName(), tag.getContainingFile()) {
@Override
public void run() {
tag.getValue().setEscapedText(value);
}
}.execute();
continue;
}
// The value didn't exist, add it.
final XmlTag child = sourceXml.createChildTag(SdkConstants.TAG_ITEM, sourceXml.getNamespace(), value, false);
child.setAttribute(SdkConstants.ATTR_NAME, attribute);
new WriteCommandAction.Simple(myProject, "Adding value of " + child.getName(), child.getContainingFile()) {
@Override
public void run() {
sourceXml.addSubTag(child, false);
}
}.execute();
}
return true;
}
/**
* Changes the name of the themes in all the xml files
* The theme needs to be reloaded in ThemeEditorComponent for the change to be complete
* THIS METHOD DOES NOT DIRECTLY MODIFY THE VALUE ONE GETS WHEN EVALUATING getParent()
*/
public void setParent(@NotNull final String newParent) {
if (!isProjectStyle()) {
throw new UnsupportedOperationException("Non project styles can not be modified");
}
for (final XmlTag sourceXml : getSourceXmls()) {
new WriteCommandAction.Simple(myProject, "Updating parent to " + newParent) {
@Override
protected void run() throws Throwable {
sourceXml.setAttribute(SdkConstants.ATTR_PARENT, newParent);
}
}.execute();
}
}
@NotNull
public StyleResolver getResolver() {
return myThemeResolver;
}
@Override
public String toString() {
if (!isReadOnly()) {
return "[" + getSimpleName() + "]";
}
return getSimpleName();
}
@Override
public boolean equals(Object obj) {
if (obj == null || (!(obj instanceof ThemeEditorStyle))) {
return false;
}
return getName().equals(((ThemeEditorStyle)obj).getName());
}
@Override
public int hashCode() {
return getName().hashCode();
}
@NotNull
public Configuration getConfiguration() {
return myConfiguration;
}
/**
* Deletes an attribute of that particular style from the xml file
*/
public void removeAttribute(@NotNull final String attribute) {
if (!isProjectStyle()) {
throw new UnsupportedOperationException("Non project styles can not be modified");
}
for (XmlTag sourceXml : getSourceXmls()) {
final XmlTag tag = getValueTag(sourceXml, attribute);
if (tag != null) {
new WriteCommandAction.Simple(myProject, "Removing " + tag.getName(), tag.getContainingFile()) {
@Override
public void run() {
tag.delete();
}
}.execute();
}
}
}
/**
* Returns a PsiElement of the name attribute for this theme
* made from a RANDOM sourceXml
*/
@Nullable
public PsiElement getNamePsiElement() {
List<XmlTag> sourceXmls = getSourceXmls();
if (sourceXmls.isEmpty()){
return null;
}
// Any sourceXml will do to get the name attribute from
final XmlAttribute nameAttribute = sourceXmls.get(0).getAttribute("name");
if (nameAttribute == null) {
return null;
}
XmlAttributeValue attributeValue = nameAttribute.getValueElement();
if (attributeValue == null) {
return null;
}
return new ValueResourceElementWrapper(attributeValue);
}
}