diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java | 628 |
1 files changed, 628 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java new file mode 100644 index 000000000..b0e3d43d0 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/resources/ResourceHelper.java @@ -0,0 +1,628 @@ +/* + * Copyright (C) 2011 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.resources; + +import static com.android.SdkConstants.ANDROID_PREFIX; +import static com.android.SdkConstants.ANDROID_STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.ANDROID_URI; +import static com.android.SdkConstants.ATTR_COLOR; +import static com.android.SdkConstants.ATTR_NAME; +import static com.android.SdkConstants.ATTR_TYPE; +import static com.android.SdkConstants.DOT_XML; +import static com.android.SdkConstants.EXT_XML; +import static com.android.SdkConstants.FD_RESOURCES; +import static com.android.SdkConstants.FD_RES_VALUES; +import static com.android.SdkConstants.PREFIX_RESOURCE_REF; +import static com.android.SdkConstants.STYLE_RESOURCE_PREFIX; +import static com.android.SdkConstants.TAG_ITEM; +import static com.android.SdkConstants.TAG_RESOURCES; +import static com.android.ide.eclipse.adt.AdtConstants.WS_SEP; + +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.resources.ResourceDeltaKind; +import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.common.resources.ResourceUrl; +import com.android.ide.common.resources.configuration.CountryCodeQualifier; +import com.android.ide.common.resources.configuration.DensityQualifier; +import com.android.ide.common.resources.configuration.FolderConfiguration; +import com.android.ide.common.resources.configuration.KeyboardStateQualifier; +import com.android.ide.common.resources.configuration.LayoutDirectionQualifier; +import com.android.ide.common.resources.configuration.LocaleQualifier; +import com.android.ide.common.resources.configuration.NavigationMethodQualifier; +import com.android.ide.common.resources.configuration.NavigationStateQualifier; +import com.android.ide.common.resources.configuration.NetworkCodeQualifier; +import com.android.ide.common.resources.configuration.NightModeQualifier; +import com.android.ide.common.resources.configuration.ResourceQualifier; +import com.android.ide.common.resources.configuration.ScreenDimensionQualifier; +import com.android.ide.common.resources.configuration.ScreenHeightQualifier; +import com.android.ide.common.resources.configuration.ScreenOrientationQualifier; +import com.android.ide.common.resources.configuration.ScreenRatioQualifier; +import com.android.ide.common.resources.configuration.ScreenSizeQualifier; +import com.android.ide.common.resources.configuration.ScreenWidthQualifier; +import com.android.ide.common.resources.configuration.SmallestScreenWidthQualifier; +import com.android.ide.common.resources.configuration.TextInputMethodQualifier; +import com.android.ide.common.resources.configuration.TouchScreenQualifier; +import com.android.ide.common.resources.configuration.UiModeQualifier; +import com.android.ide.common.resources.configuration.VersionQualifier; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor; +import com.android.ide.eclipse.adt.internal.editors.Hyperlinks; +import com.android.ide.eclipse.adt.internal.editors.IconFactory; +import com.android.ide.eclipse.adt.internal.editors.layout.gle2.ImageUtils; +import com.android.ide.eclipse.adt.internal.editors.layout.refactoring.VisualRefactoring; +import com.android.ide.eclipse.adt.internal.wizards.newxmlfile.NewXmlFileWizard; +import com.android.resources.FolderTypeRelationship; +import com.android.resources.ResourceFolderType; +import com.android.resources.ResourceType; +import com.android.utils.Pair; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.Path; +import org.eclipse.jface.text.IRegion; +import org.eclipse.jface.text.Region; +import org.eclipse.swt.graphics.Image; +import org.eclipse.swt.graphics.RGB; +import org.eclipse.wst.sse.core.StructuredModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IModelManager; +import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; +import org.eclipse.wst.sse.core.internal.provisional.IndexedRegion; +import org.eclipse.wst.sse.core.internal.provisional.text.IStructuredDocument; +import org.eclipse.wst.xml.core.internal.document.ElementImpl; +import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; +import org.w3c.dom.Attr; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.NamedNodeMap; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; +import org.w3c.dom.Text; +import org.xml.sax.InputSource; + +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; + +/** + * Helper class to deal with SWT specifics for the resources. + */ +@SuppressWarnings("restriction") // XML model +public class ResourceHelper { + + private final static Map<Class<?>, Image> sIconMap = new HashMap<Class<?>, Image>( + FolderConfiguration.getQualifierCount()); + + static { + try { + IconFactory factory = IconFactory.getInstance(); + sIconMap.put(CountryCodeQualifier.class, factory.getIcon("mcc")); //$NON-NLS-1$ + sIconMap.put(NetworkCodeQualifier.class, factory.getIcon("mnc")); //$NON-NLS-1$ + sIconMap.put(LocaleQualifier.class, factory.getIcon("language")); //$NON-NLS-1$ + sIconMap.put(LayoutDirectionQualifier.class, factory.getIcon("bidi")); //$NON-NLS-1$ + sIconMap.put(ScreenSizeQualifier.class, factory.getIcon("size")); //$NON-NLS-1$ + sIconMap.put(ScreenRatioQualifier.class, factory.getIcon("ratio")); //$NON-NLS-1$ + sIconMap.put(ScreenOrientationQualifier.class, factory.getIcon("orientation")); //$NON-NLS-1$ + sIconMap.put(UiModeQualifier.class, factory.getIcon("dockmode")); //$NON-NLS-1$ + sIconMap.put(NightModeQualifier.class, factory.getIcon("nightmode")); //$NON-NLS-1$ + sIconMap.put(DensityQualifier.class, factory.getIcon("dpi")); //$NON-NLS-1$ + sIconMap.put(TouchScreenQualifier.class, factory.getIcon("touch")); //$NON-NLS-1$ + sIconMap.put(KeyboardStateQualifier.class, factory.getIcon("keyboard")); //$NON-NLS-1$ + sIconMap.put(TextInputMethodQualifier.class, factory.getIcon("text_input")); //$NON-NLS-1$ + sIconMap.put(NavigationStateQualifier.class, factory.getIcon("navpad")); //$NON-NLS-1$ + sIconMap.put(NavigationMethodQualifier.class, factory.getIcon("navpad")); //$NON-NLS-1$ + sIconMap.put(ScreenDimensionQualifier.class, factory.getIcon("dimension")); //$NON-NLS-1$ + sIconMap.put(VersionQualifier.class, factory.getIcon("version")); //$NON-NLS-1$ + sIconMap.put(ScreenWidthQualifier.class, factory.getIcon("width")); //$NON-NLS-1$ + sIconMap.put(ScreenHeightQualifier.class, factory.getIcon("height")); //$NON-NLS-1$ + sIconMap.put(SmallestScreenWidthQualifier.class,factory.getIcon("swidth")); //$NON-NLS-1$ + } catch (Throwable t) { + AdtPlugin.log(t , null); + } + } + + /** + * Returns the icon for the qualifier. + */ + public static Image getIcon(Class<? extends ResourceQualifier> theClass) { + return sIconMap.get(theClass); + } + + /** + * Returns a {@link ResourceDeltaKind} from an {@link IResourceDelta} value. + * @param kind a {@link IResourceDelta} integer constant. + * @return a matching {@link ResourceDeltaKind} or null. + * + * @see IResourceDelta#ADDED + * @see IResourceDelta#REMOVED + * @see IResourceDelta#CHANGED + */ + public static ResourceDeltaKind getResourceDeltaKind(int kind) { + switch (kind) { + case IResourceDelta.ADDED: + return ResourceDeltaKind.ADDED; + case IResourceDelta.REMOVED: + return ResourceDeltaKind.REMOVED; + case IResourceDelta.CHANGED: + return ResourceDeltaKind.CHANGED; + } + + return null; + } + + /** + * Is this a resource that can be defined in any file within the "values" folder? + * <p> + * Some resource types can be defined <b>both</b> as a separate XML file as well + * as defined within a value XML file. This method will return true for these types + * as well. In other words, a ResourceType can return true for both + * {@link #isValueBasedResourceType} and {@link #isFileBasedResourceType}. + * + * @param type the resource type to check + * @return true if the given resource type can be represented as a value under the + * values/ folder + */ + public static boolean isValueBasedResourceType(ResourceType type) { + List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type); + for (ResourceFolderType folderType : folderTypes) { + if (folderType == ResourceFolderType.VALUES) { + return true; + } + } + + return false; + } + + /** + * Is this a resource that is defined in a file named by the resource plus the XML + * extension? + * <p> + * Some resource types can be defined <b>both</b> as a separate XML file as well as + * defined within a value XML file along with other properties. This method will + * return true for these resource types as well. In other words, a ResourceType can + * return true for both {@link #isValueBasedResourceType} and + * {@link #isFileBasedResourceType}. + * + * @param type the resource type to check + * @return true if the given resource type is stored in a file named by the resource + */ + public static boolean isFileBasedResourceType(ResourceType type) { + List<ResourceFolderType> folderTypes = FolderTypeRelationship.getRelatedFolders(type); + for (ResourceFolderType folderType : folderTypes) { + if (folderType != ResourceFolderType.VALUES) { + + if (type == ResourceType.ID) { + // The folder types for ID is not only VALUES but also + // LAYOUT and MENU. However, unlike resources, they are only defined + // inline there so for the purposes of isFileBasedResourceType + // (where the intent is to figure out files that are uniquely identified + // by a resource's name) this method should return false anyway. + return false; + } + + return true; + } + } + + return false; + } + + /** + * Returns true if this class can create the given resource + * + * @param resource the resource to be created + * @return true if the {@link #createResource} method can create this resource + */ + public static boolean canCreateResource(String resource) { + // Cannot create framework resources + if (resource.startsWith(ANDROID_PREFIX)) { + return false; + } + + ResourceUrl parsed = ResourceUrl.parse(resource); + if (parsed != null) { + if (parsed.framework) { + return false; + } + ResourceType type = parsed.type; + String name = parsed.name; + + // Make sure the name is valid + ResourceNameValidator validator = + ResourceNameValidator.create(false, (Set<String>) null /* existing */, type); + if (validator.isValid(name) != null) { + return false; + } + + return canCreateResourceType(type); + } + + return false; + } + + /** + * Returns true if this class can create resources of the given resource + * type + * + * @param type the type of resource to be created + * @return true if the {@link #createResource} method can create resources + * of this type (provided the name parameter is also valid) + */ + public static boolean canCreateResourceType(ResourceType type) { + // We can create all value types + if (isValueBasedResourceType(type)) { + return true; + } + + // We can create -some- file-based types - those supported by the New XML wizard: + for (ResourceFolderType folderType : FolderTypeRelationship.getRelatedFolders(type)) { + if (NewXmlFileWizard.canCreateXmlFile(folderType)) { + return true; + } + } + + return false; + } + + /** Creates a file-based resource, like a layout. Used by {@link #createResource} */ + private static Pair<IFile,IRegion> createFileResource(IProject project, ResourceType type, + String name) { + + ResourceFolderType folderType = null; + for (ResourceFolderType f : FolderTypeRelationship.getRelatedFolders(type)) { + if (NewXmlFileWizard.canCreateXmlFile(f)) { + folderType = f; + break; + } + } + if (folderType == null) { + return null; + } + + // Find "dimens.xml" file in res/values/ (or corresponding name for other + // value types) + IPath projectPath = new Path(FD_RESOURCES + WS_SEP + folderType.getName() + WS_SEP + + name + '.' + EXT_XML); + IFile file = project.getFile(projectPath); + return NewXmlFileWizard.createXmlFile(project, file, folderType); + } + + /** + * Creates a resource of a given type, name and (if applicable) value + * + * @param project the project to contain the resource + * @param type the type of resource + * @param name the name of the resource + * @param value the value of the resource, if it is a value-type resource + * @return a pair of the file containing the resource and a region where the value + * appears + */ + public static Pair<IFile,IRegion> createResource(IProject project, ResourceType type, + String name, String value) { + if (!isValueBasedResourceType(type)) { + return createFileResource(project, type, name); + } + + // Find "dimens.xml" file in res/values/ (or corresponding name for other + // value types) + String typeName = type.getName(); + String fileName = typeName + 's'; + String projectPath = FD_RESOURCES + WS_SEP + FD_RES_VALUES + WS_SEP + + fileName + '.' + EXT_XML; + Object editRequester = project; + IResource member = project.findMember(projectPath); + String tagName = Hyperlinks.getTagName(type); + boolean createEmptyTag = type == ResourceType.ID; + if (member != null) { + if (member instanceof IFile) { + IFile file = (IFile) member; + // File exists: Must add item to the XML + IModelManager manager = StructuredModelManager.getModelManager(); + IStructuredModel model = null; + try { + model = manager.getExistingModelForEdit(file); + if (model == null) { + model = manager.getModelForEdit(file); + } + if (model instanceof IDOMModel) { + model.beginRecording(editRequester, String.format("Add %1$s", + type.getDisplayName())); + IDOMModel domModel = (IDOMModel) model; + Document document = domModel.getDocument(); + Element root = document.getDocumentElement(); + IStructuredDocument structuredDocument = model.getStructuredDocument(); + Node lastElement = null; + NodeList childNodes = root.getChildNodes(); + String indent = null; + for (int i = childNodes.getLength() - 1; i >= 0; i--) { + Node node = childNodes.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + lastElement = node; + indent = AndroidXmlEditor.getIndent(structuredDocument, node); + break; + } + } + if (indent == null || indent.length() == 0) { + indent = " "; //$NON-NLS-1$ + } + Node nextChild = lastElement != null ? lastElement.getNextSibling() : null; + Text indentNode = document.createTextNode('\n' + indent); + root.insertBefore(indentNode, nextChild); + Element element = document.createElement(tagName); + if (createEmptyTag) { + if (element instanceof ElementImpl) { + ElementImpl elementImpl = (ElementImpl) element; + elementImpl.setEmptyTag(true); + } + } + element.setAttribute(ATTR_NAME, name); + if (!tagName.equals(typeName)) { + element.setAttribute(ATTR_TYPE, typeName); + } + root.insertBefore(element, nextChild); + IRegion region = null; + + if (createEmptyTag) { + IndexedRegion domRegion = VisualRefactoring.getRegion(element); + int endOffset = domRegion.getEndOffset(); + region = new Region(endOffset, 0); + } else { + Node valueNode = document.createTextNode(value); + element.appendChild(valueNode); + + IndexedRegion domRegion = VisualRefactoring.getRegion(valueNode); + int startOffset = domRegion.getStartOffset(); + int length = domRegion.getLength(); + region = new Region(startOffset, length); + } + model.save(); + return Pair.of(file, region); + } + } catch (Exception e) { + AdtPlugin.log(e, "Cannot access XML value model"); + } finally { + if (model != null) { + model.endRecording(editRequester); + model.releaseFromEdit(); + } + } + } + + return null; + } else { + // No such file exists: just create it + String prolog = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"; //$NON-NLS-1$ + StringBuilder sb = new StringBuilder(prolog); + + String root = TAG_RESOURCES; + sb.append('<').append(root).append('>').append('\n'); + sb.append(" "); //$NON-NLS-1$ + sb.append('<'); + sb.append(tagName); + sb.append(" name=\""); //$NON-NLS-1$ + sb.append(name); + sb.append('"'); + if (!tagName.equals(typeName)) { + sb.append(" type=\""); //$NON-NLS-1$ + sb.append(typeName); + sb.append('"'); + } + int start, end; + if (createEmptyTag) { + sb.append("/>"); //$NON-NLS-1$ + start = sb.length(); + end = sb.length(); + } else { + sb.append('>'); + start = sb.length(); + sb.append(value); + end = sb.length(); + sb.append('<').append('/'); + sb.append(tagName); + sb.append('>'); + } + sb.append('\n').append('<').append('/').append(root).append('>').append('\n'); + String result = sb.toString(); + // TODO: Pretty print string (wait until that CL is integrated) + String error = null; + try { + byte[] buf = result.getBytes("UTF8"); //$NON-NLS-1$ + InputStream stream = new ByteArrayInputStream(buf); + IFile file = project.getFile(new Path(projectPath)); + file.create(stream, true /*force*/, null /*progress*/); + IRegion region = new Region(start, end - start); + return Pair.of(file, region); + } catch (UnsupportedEncodingException e) { + error = e.getMessage(); + } catch (CoreException e) { + error = e.getMessage(); + } + + error = String.format("Failed to generate %1$s: %2$s", name, error); + AdtPlugin.displayError("New Android XML File", error); + } + return null; + } + + /** + * Returns the theme name to be shown for theme styles, e.g. for "@style/Theme" it + * returns "Theme" + * + * @param style a theme style string + * @return the user visible theme name + */ + public static String styleToTheme(String style) { + if (style.startsWith(STYLE_RESOURCE_PREFIX)) { + style = style.substring(STYLE_RESOURCE_PREFIX.length()); + } else if (style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX)) { + style = style.substring(ANDROID_STYLE_RESOURCE_PREFIX.length()); + } else if (style.startsWith(PREFIX_RESOURCE_REF)) { + // @package:style/foo + int index = style.indexOf('/'); + if (index != -1) { + style = style.substring(index + 1); + } + } + return style; + } + + /** + * Returns true if the given style represents a project theme + * + * @param style a theme style string + * @return true if the style string represents a project theme, as opposed + * to a framework theme + */ + public static boolean isProjectStyle(String style) { + assert style.startsWith(STYLE_RESOURCE_PREFIX) + || style.startsWith(ANDROID_STYLE_RESOURCE_PREFIX) : style; + + return style.startsWith(STYLE_RESOURCE_PREFIX); + } + + /** + * Returns the layout resource name for the given layout file, e.g. for + * /res/layout/foo.xml returns foo. + * + * @param layoutFile the layout file whose name we want to look up + * @return the layout name + */ + public static String getLayoutName(IFile layoutFile) { + String layoutName = layoutFile.getName(); + int dotIndex = layoutName.indexOf('.'); + if (dotIndex != -1) { + layoutName = layoutName.substring(0, dotIndex); + } + return layoutName; + } + + /** + * Tries to resolve the given resource value to an actual RGB color. For state lists + * it will pick the simplest/fallback color. + * + * @param resources the resource resolver to use to follow color references + * @param color the color to resolve + * @return the corresponding {@link RGB} color, or null + */ + public static RGB resolveColor(ResourceResolver resources, ResourceValue color) { + color = resources.resolveResValue(color); + if (color == null) { + return null; + } + String value = color.getValue(); + + while (value != null) { + if (value.startsWith("#")) { //$NON-NLS-1$ + try { + int rgba = ImageUtils.getColor(value); + // Drop alpha channel + return ImageUtils.intToRgb(rgba); + } catch (NumberFormatException nfe) { + // Pass + } + return null; + } + if (value.startsWith(PREFIX_RESOURCE_REF)) { + boolean isFramework = color.isFramework(); + color = resources.findResValue(value, isFramework); + if (color != null) { + value = color.getValue(); + } else { + break; + } + } else { + File file = new File(value); + if (file.exists() && file.getName().endsWith(DOT_XML)) { + // Parse + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + BufferedInputStream bis = null; + try { + bis = new BufferedInputStream(new FileInputStream(file)); + InputSource is = new InputSource(bis); + factory.setNamespaceAware(true); + factory.setValidating(false); + DocumentBuilder builder = factory.newDocumentBuilder(); + Document document = builder.parse(is); + NodeList items = document.getElementsByTagName(TAG_ITEM); + + value = findColorValue(items); + continue; + } catch (Exception e) { + AdtPlugin.log(e, "Failed parsing color file %1$s", file.getName()); + } finally { + if (bis != null) { + try { + bis.close(); + } catch (IOException e) { + // Nothing useful can be done here + } + } + } + } + + return null; + } + } + + return null; + } + + /** + * Searches a color XML file for the color definition element that does not + * have an associated state and returns its color + */ + private static String findColorValue(NodeList items) { + for (int i = 0, n = items.getLength(); i < n; i++) { + // Find non-state color definition + Node item = items.item(i); + boolean hasState = false; + if (item.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) item; + if (element.hasAttributeNS(ANDROID_URI, ATTR_COLOR)) { + NamedNodeMap attributes = element.getAttributes(); + for (int j = 0, m = attributes.getLength(); j < m; j++) { + Attr attribute = (Attr) attributes.item(j); + if (attribute.getLocalName().startsWith("state_")) { //$NON-NLS-1$ + hasState = true; + break; + } + } + + if (!hasState) { + return element.getAttributeNS(ANDROID_URI, ATTR_COLOR); + } + } + } + } + + return null; + } +} |