blob: 169c1dd1e0c1a8a55729e424fc80527d8d49a1aa [file] [log] [blame]
/*
* Copyright 2000-2012 JetBrains s.r.o.
*
* 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.intellij.android.designer.model;
import com.android.resources.ResourceType;
import com.android.tools.idea.AndroidPsiUtils;
import com.android.tools.idea.rendering.AppResourceRepository;
import com.android.tools.idea.rendering.ResourceHelper;
import com.google.common.collect.Lists;
import com.intellij.designer.model.RadComponent;
import com.intellij.designer.model.RadComponentVisitor;
import com.intellij.lang.LanguageNamesValidation;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.lang.refactoring.NamesValidator;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlTag;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import static com.android.SdkConstants.*;
/**
* The ID manager is responsible for assigning and reassigning id's for layout widgets
*/
public class IdManager {
/** Returns an ID manager */
@NotNull
public static IdManager get() {
return new IdManager();
}
/** Looks up the id base name from the given id attribute value, e.g. for {@code @+id/foo} this returns {@code foo} */
@Nullable
public static String getIdName(@Nullable String idValue) {
if (idValue != null) {
if (idValue.startsWith(NEW_ID_PREFIX)) {
return idValue.substring(NEW_ID_PREFIX.length());
}
else if (idValue.startsWith(ID_PREFIX)) {
return idValue.substring(ID_PREFIX.length());
}
}
return null;
}
/** Looks up the existing set of id's reachable from the component's context */
private static Collection<String> getIds(RadViewComponent component) {
XmlTag tag = component.getTag();
Module module = AndroidPsiUtils.getModuleSafely(tag);
return getIds(module);
}
/** Looks up the existing set of id's reachable from the given module */
private static Collection<String> getIds(@Nullable Module module) {
if (module != null) {
AppResourceRepository resources = AppResourceRepository.getAppResources(module, true);
if (resources != null) {
return resources.getItemsOfType(ResourceType.ID);
}
}
return Collections.emptyList();
}
/**
* Assign a suitable new and unique id to the given component.
*/
@NotNull
public String assignId(RadViewComponent component) {
XmlTag tag = component.getTag();
Collection<String> idList = getIds(AndroidPsiUtils.getModuleSafely(tag));
return assignId(component, idList);
}
/**
* Assign a suitable new and unique id to the given component. The set of
* existing id's is provided in the given list.
*/
@NotNull
public String assignId(RadViewComponent component, Collection<String> idList) {
String idValue = StringUtil.decapitalize(component.getMetaModel().getTag());
XmlTag tag = component.getTag();
Module module = AndroidPsiUtils.getModuleSafely(tag);
if (module != null) {
idValue = ResourceHelper.prependResourcePrefix(module, idValue);
}
String nextIdValue = idValue;
int index = 0;
// Ensure that we don't create something like "switch" as an id, which won't compile when used
// in the R class
NamesValidator validator = LanguageNamesValidation.INSTANCE.forLanguage(JavaLanguage.INSTANCE);
Project project = tag.getProject();
while (idList.contains(nextIdValue) || validator != null && validator.isKeyword(nextIdValue, project)) {
++index;
if (index == 1 && (validator == null || !validator.isKeyword(nextIdValue, project))) {
nextIdValue = idValue;
} else {
nextIdValue = idValue + Integer.toString(index);
}
}
String newId = NEW_ID_PREFIX + idValue + (index == 0 ? "" : Integer.toString(index));
tag.setAttribute(ATTR_ID, ANDROID_URI, newId);
return newId;
}
/**
* Determines whether the given new component should have an id attribute.
* This is generally false for layouts, and generally true for other views,
* not including the {@code <include>} and {@code <merge>} tags. Note that
* {@code <fragment>} tags <b>should</b> specify an id.
*
* @param component the new component to check
* @return true if the component should have a default id
*/
public boolean needsDefaultId(RadViewComponent component) {
if (component instanceof RadViewContainer) {
return false;
}
String tag = component.getTag().getName();
if (tag.equals(VIEW_INCLUDE) || tag.equals(VIEW_MERGE) || tag.equals(SPACE) || tag.equals(REQUEST_FOCUS) ||
// Handle <Space> in the compatibility library package
(tag.endsWith(SPACE) && tag.length() > SPACE.length() && tag.charAt(tag.length() - SPACE.length()) == '.')) {
return false;
}
return true;
}
/**
* Ensure that the given component <b>hierarchy</b> has unique id's and that
* widgets which need an id have been assigned one.
* <p>
* This is most important after copy/paste. If you copy a component hierarchy,
* and then paste a second copy, all the ids must be changed to be unique, and
* more importantly, all the <b>references</b> to these components must be updated
* as well!
*
* @param container the root container to recursively update
*/
public void ensureIds(final RadViewComponent container) {
ApplicationManager.getApplication().runWriteAction(new Runnable() {
@Override
public void run() {
final List<Pair<Pair<String, String>, String>> replaceList = new ArrayList<Pair<Pair<String, String>, String>>();
final List<String> idList = Lists.newArrayList(getIds(container));
container.accept(new RadComponentVisitor() {
@Override
public void endVisit(RadComponent component) {
RadViewComponent viewComponent = (RadViewComponent)component;
String id = viewComponent.getId();
String idName = getIdName(id);
if (component == container) {
if (idName != null || needsDefaultId(viewComponent)) {
id = assignId(viewComponent, idList);
idList.add(getIdName(id));
}
}
else if (idName != null && idList.contains(idName)) {
id = assignId(viewComponent, idList);
idList.add(getIdName(id));
// Rename all @id/ and @+id/ references from the old name to the new name
replaceList.add(Pair.create(Pair.create(ID_PREFIX + idName, NEW_ID_PREFIX + idName), id));
}
}
}, true);
if (!replaceList.isEmpty()) {
replaceIds(container, replaceList);
}
}
});
}
/** For strings A, B and C in {@code Pair<Pair<A,B>,C>} this will replace occurrences of A or B with C */
private static void replaceIds(RadViewComponent container, final List<Pair<Pair<String, String>, String>> replaceList) {
container.accept(new RadComponentVisitor() {
@Override
public void endVisit(RadComponent component) {
XmlTag tag = ((RadViewComponent)component).getTag();
for (XmlAttribute attribute : tag.getAttributes()) {
String value = attribute.getValue();
for (Pair<Pair<String, String>, String> replace : replaceList) {
if (replace.first.first.equals(value) || replace.first.second.equals(value)) {
attribute.setValue(replace.second);
break;
}
}
}
}
}, true);
}
}