diff options
Diffstat (limited to 'eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java')
-rw-r--r-- | eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java | 642 |
1 files changed, 642 insertions, 0 deletions
diff --git a/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java new file mode 100644 index 000000000..5661b2919 --- /dev/null +++ b/eclipse/plugins/com.android.ide.eclipse.adt/src/com/android/ide/eclipse/adt/internal/editors/layout/gle2/PreviewIconFactory.java @@ -0,0 +1,642 @@ +/* + * 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.editors.layout.gle2; + +import static com.android.SdkConstants.DOT_PNG; +import static com.android.SdkConstants.FQCN_DATE_PICKER; +import static com.android.SdkConstants.FQCN_EXPANDABLE_LIST_VIEW; +import static com.android.SdkConstants.FQCN_LIST_VIEW; +import static com.android.SdkConstants.FQCN_TIME_PICKER; + +import com.android.annotations.NonNull; +import com.android.annotations.Nullable; +import com.android.ide.common.rendering.LayoutLibrary; +import com.android.ide.common.rendering.api.Capability; +import com.android.ide.common.rendering.api.RenderSession; +import com.android.ide.common.rendering.api.ResourceValue; +import com.android.ide.common.rendering.api.SessionParams.RenderingMode; +import com.android.ide.common.rendering.api.StyleResourceValue; +import com.android.ide.common.rendering.api.ViewInfo; +import com.android.ide.common.resources.ResourceResolver; +import com.android.ide.eclipse.adt.AdtPlugin; +import com.android.ide.eclipse.adt.internal.editors.descriptors.DocumentDescriptor; +import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditorDelegate; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.PaletteMetadataDescriptor; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository; +import com.android.ide.eclipse.adt.internal.editors.layout.gre.ViewMetadataRepository.RenderMode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiDocumentNode; +import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode; +import com.android.ide.eclipse.adt.internal.resources.ResourceHelper; +import com.android.ide.eclipse.adt.internal.sdk.AndroidTargetData; +import com.android.sdklib.IAndroidTarget; +import com.android.utils.Pair; + +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IStatus; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.swt.graphics.RGB; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; + +import java.awt.image.BufferedImage; +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Properties; + +import javax.imageio.ImageIO; + +/** + * Factory which can provide preview icons for android views of a particular SDK and + * editor's configuration chooser + */ +public class PreviewIconFactory { + private PaletteControl mPalette; + private RGB mBackground; + private RGB mForeground; + private File mImageDir; + + private static final String PREVIEW_INFO_FILE = "preview.properties"; //$NON-NLS-1$ + + public PreviewIconFactory(PaletteControl palette) { + mPalette = palette; + } + + /** + * Resets the state in the preview icon factory such that it will re-fetch information + * like the theme and SDK (the icons themselves are cached in a directory across IDE + * session though) + */ + public void reset() { + mImageDir = null; + mBackground = null; + mForeground = null; + } + + /** + * Deletes all the persistent state for the current settings such that it will be regenerated + */ + public void refresh() { + File imageDir = getImageDir(false); + if (imageDir != null && imageDir.exists()) { + File[] files = imageDir.listFiles(); + for (File file : files) { + file.delete(); + } + imageDir.delete(); + reset(); + } + } + + /** + * Returns an image descriptor for the given element descriptor, or null if no image + * could be computed. The rendering parameters (SDK, theme etc) correspond to those + * stored in the associated palette. + * + * @param desc the element descriptor to get an image for + * @return an image descriptor, or null if no image could be rendered + */ + public ImageDescriptor getImageDescriptor(ElementDescriptor desc) { + File imageDir = getImageDir(false); + if (!imageDir.exists()) { + render(); + } + File file = new File(imageDir, getFileName(desc)); + if (file.exists()) { + try { + return ImageDescriptor.createFromURL(file.toURI().toURL()); + } catch (MalformedURLException e) { + AdtPlugin.log(e, "Could not create image descriptor for %s", file); + } + } + + return null; + } + + /** + * Partition the elements in the document according to their rendering preferences; + * elements that should be skipped are removed, elements that should be rendered alone + * are placed in their own list, etc + * + * @param document the document containing render fragments for the various elements + * @return + */ + private List<List<Element>> partitionRenderElements(Document document) { + List<List<Element>> elements = new ArrayList<List<Element>>(); + + List<Element> shared = new ArrayList<Element>(); + Element root = document.getDocumentElement(); + elements.add(shared); + + ViewMetadataRepository repository = ViewMetadataRepository.get(); + + NodeList children = root.getChildNodes(); + for (int i = 0, n = children.getLength(); i < n; i++) { + Node node = children.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + Element element = (Element) node; + String fqn = repository.getFullClassName(element); + assert fqn.length() > 0 : element.getNodeName(); + RenderMode renderMode = repository.getRenderMode(fqn); + + // Temporary special cases + if (fqn.equals(FQCN_LIST_VIEW) || fqn.equals(FQCN_EXPANDABLE_LIST_VIEW)) { + if (!mPalette.getEditor().renderingSupports(Capability.ADAPTER_BINDING)) { + renderMode = RenderMode.SKIP; + } + } else if (fqn.equals(FQCN_DATE_PICKER) || fqn.equals(FQCN_TIME_PICKER)) { + IAndroidTarget renderingTarget = mPalette.getEditor().getRenderingTarget(); + // In Honeycomb, these widgets only render properly in the Holo themes. + int apiLevel = renderingTarget.getVersion().getApiLevel(); + if (apiLevel == 11) { + String themeName = mPalette.getCurrentTheme(); + if (themeName == null || !themeName.startsWith("Theme.Holo")) { //$NON-NLS-1$ + // Note - it's possible that the the theme is some other theme + // such as a user theme which inherits from Theme.Holo and that + // the render -would- have worked, but it's harder to detect that + // scenario, so we err on the side of caution and just show an + // icon + name for the time widgets. + renderMode = RenderMode.SKIP; + } + } else if (apiLevel >= 12) { + // Currently broken, even for Holo. + renderMode = RenderMode.SKIP; + } // apiLevel <= 10 is fine + } + + if (renderMode == RenderMode.ALONE) { + elements.add(Collections.singletonList(element)); + } else if (renderMode == RenderMode.NORMAL) { + shared.add(element); + } else { + assert renderMode == RenderMode.SKIP; + } + } + } + + return elements; + } + + /** + * Renders ALL the widgets and then extracts image data for each view and saves it on + * disk + */ + private boolean render() { + File imageDir = getImageDir(true); + + GraphicalEditorPart editor = mPalette.getEditor(); + LayoutEditorDelegate layoutEditorDelegate = editor.getEditorDelegate(); + LayoutLibrary layoutLibrary = editor.getLayoutLibrary(); + Integer overrideBgColor = null; + if (layoutLibrary != null) { + if (layoutLibrary.supports(Capability.CUSTOM_BACKGROUND_COLOR)) { + Pair<RGB, RGB> themeColors = getColorsFromTheme(); + RGB bg = themeColors.getFirst(); + RGB fg = themeColors.getSecond(); + if (bg != null) { + storeBackground(imageDir, bg, fg); + overrideBgColor = Integer.valueOf(ImageUtils.rgbToInt(bg, 0xFF)); + } + } + } + + ViewMetadataRepository repository = ViewMetadataRepository.get(); + Document document = repository.getRenderingConfigDoc(); + + if (document == null) { + return false; + } + + // Construct UI model from XML + AndroidTargetData data = layoutEditorDelegate.getEditor().getTargetData(); + DocumentDescriptor documentDescriptor; + if (data == null) { + documentDescriptor = new DocumentDescriptor("temp", null/*children*/);//$NON-NLS-1$ + } else { + documentDescriptor = data.getLayoutDescriptors().getDescriptor(); + } + UiDocumentNode model = (UiDocumentNode) documentDescriptor.createUiNode(); + model.setEditor(layoutEditorDelegate.getEditor()); + model.setUnknownDescriptorProvider(editor.getModel().getUnknownDescriptorProvider()); + + Element documentElement = document.getDocumentElement(); + List<List<Element>> elements = partitionRenderElements(document); + for (List<Element> elementGroup : elements) { + // Replace the document elements with the current element group + while (documentElement.getFirstChild() != null) { + documentElement.removeChild(documentElement.getFirstChild()); + } + for (Element element : elementGroup) { + documentElement.appendChild(element); + } + + model.loadFromXmlNode(document); + + RenderSession session = null; + NodeList childNodes = documentElement.getChildNodes(); + try { + // Important to get these sizes large enough for clients that don't support + // RenderMode.FULL_EXPAND such as 1.6 + int width = 200; + int height = childNodes.getLength() == 1 ? 400 : 1600; + + session = RenderService.create(editor) + .setModel(model) + .setOverrideRenderSize(width, height) + .setRenderingMode(RenderingMode.FULL_EXPAND) + .setLog(editor.createRenderLogger("palette")) + .setOverrideBgColor(overrideBgColor) + .setDecorations(false) + .createRenderSession(); + } catch (Throwable t) { + // If there are internal errors previewing the components just revert to plain + // icons and labels + continue; + } + + if (session != null) { + if (session.getResult().isSuccess()) { + BufferedImage image = session.getImage(); + if (image != null && image.getWidth() > 0 && image.getHeight() > 0) { + + // Fallback for older platforms where we couldn't do background rendering + // at the beginning of this method + if (mBackground == null) { + Pair<RGB, RGB> themeColors = getColorsFromTheme(); + RGB bg = themeColors.getFirst(); + RGB fg = themeColors.getSecond(); + + if (bg == null) { + // Just use a pixel from the rendering instead. + int p = image.getRGB(image.getWidth() - 1, image.getHeight() - 1); + // However, in this case we don't trust the foreground color + // even if one was found in the themes; pick one that is guaranteed + // to contrast with the background + bg = ImageUtils.intToRgb(p); + if (ImageUtils.getBrightness(ImageUtils.rgbToInt(bg, 255)) < 128) { + fg = new RGB(255, 255, 255); + } else { + fg = new RGB(0, 0, 0); + } + } + storeBackground(imageDir, bg, fg); + assert mBackground != null; + } + + List<ViewInfo> viewInfoList = session.getRootViews(); + if (viewInfoList != null && viewInfoList.size() > 0) { + // We don't render previews under a <merge> so there should + // only be one root. + ViewInfo firstRoot = viewInfoList.get(0); + int parentX = firstRoot.getLeft(); + int parentY = firstRoot.getTop(); + List<ViewInfo> infos = firstRoot.getChildren(); + for (ViewInfo info : infos) { + Object cookie = info.getCookie(); + if (!(cookie instanceof UiElementNode)) { + continue; + } + UiElementNode node = (UiElementNode) cookie; + String fileName = getFileName(node); + File file = new File(imageDir, fileName); + if (file.exists()) { + // On Windows, perhaps we need to rename instead? + file.delete(); + } + int x1 = parentX + info.getLeft(); + int y1 = parentY + info.getTop(); + int x2 = parentX + info.getRight(); + int y2 = parentY + info.getBottom(); + if (x1 != x2 && y1 != y2) { + savePreview(file, image, x1, y1, x2, y2); + } + } + } + } + } else { + StringBuilder sb = new StringBuilder(); + for (int i = 0, n = childNodes.getLength(); i < n; i++) { + Node node = childNodes.item(i); + if (node instanceof Element) { + Element e = (Element) node; + String fqn = repository.getFullClassName(e); + fqn = fqn.substring(fqn.lastIndexOf('.') + 1); + if (sb.length() > 0) { + sb.append(", "); //$NON-NLS-1$ + } + sb.append(fqn); + } + } + AdtPlugin.log(IStatus.WARNING, "Failed to render set of icons for %1$s", + sb.toString()); + + if (session.getResult().getException() != null) { + AdtPlugin.log(session.getResult().getException(), + session.getResult().getErrorMessage()); + } else if (session.getResult().getErrorMessage() != null) { + AdtPlugin.log(IStatus.WARNING, session.getResult().getErrorMessage()); + } + } + + session.dispose(); + } + } + + mPalette.getEditor().recomputeLayout(); + + return true; + } + + /** + * Look up the background and foreground colors from the theme. May not find either + * the background or foreground or both, but will always return a pair of possibly + * null colors. + * + * @return a pair of possibly null color descriptions + */ + @NonNull + private Pair<RGB, RGB> getColorsFromTheme() { + RGB background = null; + RGB foreground = null; + + ResourceResolver resources = mPalette.getEditor().getResourceResolver(); + if (resources == null) { + return Pair.of(background, foreground); + } + StyleResourceValue theme = resources.getCurrentTheme(); + if (theme != null) { + background = resolveThemeColor(resources, "windowBackground"); //$NON-NLS-1$ + if (background == null) { + background = renderDrawableResource("windowBackground"); //$NON-NLS-1$ + // This causes some harm with some themes: We'll find a color, say black, + // that isn't actually rendered in the theme. Better to use null here, + // which will cause the caller to pick a pixel from the observed background + // instead. + //if (background == null) { + // background = resolveThemeColor(resources, "colorBackground"); //$NON-NLS-1$ + //} + } + foreground = resolveThemeColor(resources, "textColorPrimary"); //$NON-NLS-1$ + } + + // Ensure that the foreground color is suitably distinct from the background color + if (background != null) { + int bgRgb = ImageUtils.rgbToInt(background, 0xFF); + int backgroundBrightness = ImageUtils.getBrightness(bgRgb); + if (foreground == null) { + if (backgroundBrightness < 128) { + foreground = new RGB(255, 255, 255); + } else { + foreground = new RGB(0, 0, 0); + } + } else { + int fgRgb = ImageUtils.rgbToInt(foreground, 0xFF); + int foregroundBrightness = ImageUtils.getBrightness(fgRgb); + if (Math.abs(backgroundBrightness - foregroundBrightness) < 64) { + if (backgroundBrightness < 128) { + foreground = new RGB(255, 255, 255); + } else { + foreground = new RGB(0, 0, 0); + } + } + } + } + + return Pair.of(background, foreground); + } + + /** + * Renders the given resource which should refer to a drawable and returns a + * representative color value for the drawable (such as the color in the center) + * + * @param themeItemName the item in the theme to be looked up and rendered + * @return a color representing a typical color in the drawable + */ + private RGB renderDrawableResource(String themeItemName) { + GraphicalEditorPart editor = mPalette.getEditor(); + ResourceResolver resources = editor.getResourceResolver(); + ResourceValue resourceValue = resources.findItemInTheme(themeItemName); + BufferedImage image = RenderService.create(editor) + .setOverrideRenderSize(100, 100) + .renderDrawable(resourceValue); + if (image != null) { + // Use the middle pixel as the color since that works better for gradients; + // solid colors work too. + int rgb = image.getRGB(image.getWidth() / 2, image.getHeight() / 2); + return ImageUtils.intToRgb(rgb); + } + + return null; + } + + private static RGB resolveThemeColor(ResourceResolver resources, String resourceName) { + ResourceValue textColor = resources.findItemInTheme(resourceName); + return ResourceHelper.resolveColor(resources, textColor); + } + + private String getFileName(ElementDescriptor descriptor) { + if (descriptor instanceof PaletteMetadataDescriptor) { + PaletteMetadataDescriptor pmd = (PaletteMetadataDescriptor) descriptor; + StringBuilder sb = new StringBuilder(); + String name = pmd.getUiName(); + // Strip out whitespace, parentheses, etc. + for (int i = 0, n = name.length(); i < n; i++) { + char c = name.charAt(i); + if (Character.isLetter(c)) { + sb.append(c); + } + } + return sb.toString() + DOT_PNG; + } + return descriptor.getUiName() + DOT_PNG; + } + + private String getFileName(UiElementNode node) { + ViewMetadataRepository repository = ViewMetadataRepository.get(); + String fqn = repository.getFullClassName((Element) node.getXmlNode()); + return fqn.substring(fqn.lastIndexOf('.') + 1) + DOT_PNG; + } + + /** + * Cleans up a name by removing punctuation and whitespace etc to make + * it a better filename + * @param name the name to clean + * @return a cleaned up name + */ + @NonNull + private static String cleanup(@Nullable String name) { + if (name == null) { + return ""; + } + + // Extract just the characters (no whitespace, parentheses, punctuation etc) + // to ensure that the filename is pretty portable + StringBuilder sb = new StringBuilder(name.length()); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + if (Character.isJavaIdentifierPart(c)) { + sb.append(Character.toLowerCase(c)); + } + } + + return sb.toString(); + } + + /** Returns the location of a directory containing image previews (which may not exist) */ + private File getImageDir(boolean create) { + if (mImageDir == null) { + // Location for plugin-related state data + IPath pluginState = AdtPlugin.getDefault().getStateLocation(); + + // We have multiple directories - one for each combination of SDK, theme and device + // (and later, possibly other qualifiers). + // These are created -lazily-. + String targetName = mPalette.getCurrentTarget().hashString(); + String androidTargetNamePrefix = "android-"; + String themeNamePrefix = "Theme."; + if (targetName.startsWith(androidTargetNamePrefix)) { + targetName = targetName.substring(androidTargetNamePrefix.length()); + } + String themeName = mPalette.getCurrentTheme(); + if (themeName == null) { + themeName = "Theme"; //$NON-NLS-1$ + } + if (themeName.startsWith(themeNamePrefix)) { + themeName = themeName.substring(themeNamePrefix.length()); + } + targetName = cleanup(targetName); + themeName = cleanup(themeName); + String deviceName = cleanup(mPalette.getCurrentDevice()); + String dirName = String.format("palette-preview-r16b-%s-%s-%s", targetName, + themeName, deviceName); + IPath dirPath = pluginState.append(dirName); + + mImageDir = new File(dirPath.toOSString()); + } + + if (create && !mImageDir.exists()) { + mImageDir.mkdirs(); + } + + return mImageDir; + } + + private void savePreview(File output, BufferedImage image, + int left, int top, int right, int bottom) { + try { + BufferedImage im = ImageUtils.subImage(image, left, top, right, bottom); + ImageIO.write(im, "PNG", output); //$NON-NLS-1$ + } catch (IOException e) { + AdtPlugin.log(e, "Failed writing palette file"); + } + } + + private void storeBackground(File imageDir, RGB bg, RGB fg) { + mBackground = bg; + mForeground = fg; + File file = new File(imageDir, PREVIEW_INFO_FILE); + String colors = String.format( + "background=#%02x%02x%02x\nforeground=#%02x%02x%02x\n", //$NON-NLS-1$ + bg.red, bg.green, bg.blue, + fg.red, fg.green, fg.blue); + AdtPlugin.writeFile(file, colors); + } + + public RGB getBackgroundColor() { + if (mBackground == null) { + initColors(); + } + + return mBackground; + } + + public RGB getForegroundColor() { + if (mForeground == null) { + initColors(); + } + + return mForeground; + } + + public void initColors() { + try { + // Already initialized? Foreground can be null which would call + // initColors again and again, but background is never null after + // initialization so we use it as the have-initialized flag. + if (mBackground != null) { + return; + } + + File imageDir = getImageDir(false); + if (!imageDir.exists()) { + render(); + + // Initialized as part of the render + if (mBackground != null) { + return; + } + } + + File file = new File(imageDir, PREVIEW_INFO_FILE); + if (file.exists()) { + Properties properties = new Properties(); + InputStream is = null; + try { + is = new BufferedInputStream(new FileInputStream(file)); + properties.load(is); + } catch (IOException e) { + AdtPlugin.log(e, "Can't read preview properties"); + } finally { + if (is != null) { + try { + is.close(); + } catch (IOException e) { + // Nothing useful can be done. + } + } + } + + String colorString = (String) properties.get("background"); //$NON-NLS-1$ + if (colorString != null) { + int rgb = ImageUtils.getColor(colorString.trim()); + mBackground = ImageUtils.intToRgb(rgb); + } + colorString = (String) properties.get("foreground"); //$NON-NLS-1$ + if (colorString != null) { + int rgb = ImageUtils.getColor(colorString.trim()); + mForeground = ImageUtils.intToRgb(rgb); + } + } + + if (mBackground == null) { + mBackground = new RGB(0, 0, 0); + } + // mForeground is allowed to be null. + } catch (Throwable t) { + AdtPlugin.log(t, "Cannot initialize preview color settings"); + } + } +} |