blob: 41807f82b71347e10af30f05c73afa0c7eda7848 [file] [log] [blame]
/*
* Copyright (C) 2008 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.ide.eclipse.adt.internal.editors;
import com.android.SdkConstants;
import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.ui.ErrorImageComposite;
import com.google.common.collect.Maps;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.RGB;
import org.eclipse.swt.widgets.Display;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import java.net.URL;
import java.util.IdentityHashMap;
import java.util.Map;
/**
* Factory to generate icons for Android Editors.
* <p/>
* Icons are kept here and reused.
*/
public class IconFactory {
public static final int COLOR_RED = SWT.COLOR_DARK_RED;
public static final int COLOR_GREEN = SWT.COLOR_DARK_GREEN;
public static final int COLOR_BLUE = SWT.COLOR_DARK_BLUE;
public static final int COLOR_DEFAULT = SWT.COLOR_BLACK;
public static final int SHAPE_CIRCLE = 'C';
public static final int SHAPE_RECT = 'R';
public static final int SHAPE_DEFAULT = SHAPE_CIRCLE;
private static IconFactory sInstance;
private Map<String, Image> mIconMap = Maps.newHashMap();
private Map<URL, Image> mUrlMap = Maps.newHashMap();
private Map<String, ImageDescriptor> mImageDescMap = Maps.newHashMap();
private Map<Image, Image> mErrorIcons;
private Map<Image, Image> mWarningIcons;
private IconFactory() {
}
public static synchronized IconFactory getInstance() {
if (sInstance == null) {
sInstance = new IconFactory();
}
return sInstance;
}
public void dispose() {
// Dispose icons
for (Image icon : mIconMap.values()) {
// The map can contain null values
if (icon != null) {
icon.dispose();
}
}
mIconMap.clear();
for (Image icon : mUrlMap.values()) {
// The map can contain null values
if (icon != null) {
icon.dispose();
}
}
mUrlMap.clear();
if (mErrorIcons != null) {
for (Image icon : mErrorIcons.values()) {
// The map can contain null values
if (icon != null) {
icon.dispose();
}
}
mErrorIcons = null;
}
if (mWarningIcons != null) {
for (Image icon : mWarningIcons.values()) {
// The map can contain null values
if (icon != null) {
icon.dispose();
}
}
mWarningIcons = null;
}
}
/**
* Returns an Image for a given icon name.
* <p/>
* Callers should not dispose it.
*
* @param osName The leaf name, without the extension, of an existing icon in the
* editor's "icons" directory. If it doesn't exists, a default icon will be
* generated automatically based on the name.
*/
public Image getIcon(String osName) {
return getIcon(osName, COLOR_DEFAULT, SHAPE_DEFAULT);
}
/**
* Returns an Image for a given icon name.
* <p/>
* Callers should not dispose it.
*
* @param osName The leaf name, without the extension, of an existing icon in the
* editor's "icons" directory. If it doesn't exist, a default icon will be
* generated automatically based on the name.
* @param color The color of the text in the automatically generated icons,
* one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED.
* @param shape The shape of the icon in the automatically generated icons,
* one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT.
*/
public Image getIcon(String osName, int color, int shape) {
String key = Character.toString((char) shape) + Integer.toString(color) + osName;
Image icon = mIconMap.get(key);
if (icon == null && !mIconMap.containsKey(key)) {
ImageDescriptor id = getImageDescriptor(osName, color, shape);
if (id != null) {
icon = id.createImage();
}
// Note that we store null references in the icon map, to avoid looking them
// up every time. If it didn't exist once, it will not exist later.
mIconMap.put(key, icon);
}
return icon;
}
/**
* Returns an ImageDescriptor for a given icon name.
* <p/>
* Callers should not dispose it.
*
* @param osName The leaf name, without the extension, of an existing icon in the
* editor's "icons" directory. If it doesn't exists, a default icon will be
* generated automatically based on the name.
*/
public ImageDescriptor getImageDescriptor(String osName) {
return getImageDescriptor(osName, COLOR_DEFAULT, SHAPE_DEFAULT);
}
/**
* Returns an ImageDescriptor for a given icon name.
* <p/>
* Callers should not dispose it.
*
* @param osName The leaf name, without the extension, of an existing icon in the
* editor's "icons" directory. If it doesn't exists, a default icon will be
* generated automatically based on the name.
* @param color The color of the text in the automatically generated icons.
* one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED.
* @param shape The shape of the icon in the automatically generated icons,
* one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT.
*/
public ImageDescriptor getImageDescriptor(String osName, int color, int shape) {
String key = Character.toString((char) shape) + Integer.toString(color) + osName;
ImageDescriptor id = mImageDescMap.get(key);
if (id == null && !mImageDescMap.containsKey(key)) {
id = AbstractUIPlugin.imageDescriptorFromPlugin(
AdtPlugin.PLUGIN_ID,
String.format("/icons/%1$s.png", osName)); //$NON-NLS-1$
if (id == null) {
id = new LetterImageDescriptor(osName.charAt(0), color, shape);
}
// Note that we store null references in the icon map, to avoid looking them
// up every time. If it didn't exist once, it will not exist later.
mImageDescMap.put(key, id);
}
return id;
}
/**
* Returns an Image for a given icon name.
* <p/>
* Callers should not dispose it.
*
* @param osName The leaf name, without the extension, of an existing icon
* in the editor's "icons" directory. If it doesn't exist, the
* fallback will be used instead.
* @param fallback the fallback icon name to use if the primary icon does
* not exist, or null if the method should return null if the
* image does not exist
* @return the icon, which should not be disposed by the caller, or null
* if the image does not exist *and*
*/
@Nullable
public Image getIcon(@NonNull String osName, @Nullable String fallback) {
String key = osName;
Image icon = mIconMap.get(key);
if (icon == null && !mIconMap.containsKey(key)) {
ImageDescriptor id = getImageDescriptor(osName, fallback);
if (id != null) {
icon = id.createImage();
}
// Note that we store null references in the icon map, to avoid looking them
// up every time. If it didn't exist once, it will not exist later.
mIconMap.put(key, icon);
}
return icon;
}
/**
* Returns an icon of the given name, or if that image does not exist and
* icon of the given fallback name.
*
* @param key the icon name
* @param fallbackKey the fallback image to use if the primary key does not
* exist
* @return the image descriptor, or null if the image does not exist and the
* fallbackKey is null
*/
@Nullable
public ImageDescriptor getImageDescriptor(@NonNull String key, @Nullable String fallbackKey) {
ImageDescriptor id = mImageDescMap.get(key);
if (id == null && !mImageDescMap.containsKey(key)) {
id = AbstractUIPlugin.imageDescriptorFromPlugin(
AdtPlugin.PLUGIN_ID,
String.format("/icons/%1$s.png", key)); //$NON-NLS-1$
if (id == null) {
if (fallbackKey == null) {
return null;
}
id = getImageDescriptor(fallbackKey);
}
// Place the fallback image for this key as well such that we don't keep trying
// to load the failed image
mImageDescMap.put(key, id);
}
return id;
}
/**
* Returns the image indicated by the given URL
*
* @param url the url to the image resources
* @return the image for the url, or null if it cannot be initialized
*/
public Image getIcon(URL url) {
Image image = mUrlMap.get(url);
if (image == null) {
ImageDescriptor descriptor = ImageDescriptor.createFromURL(url);
image = descriptor.createImage();
mUrlMap.put(url, image);
}
return image;
}
/**
* Returns an image with an error icon overlaid on it. The icons are cached,
* so the base image should be cached as well, or this method will keep
* storing new overlays into its cache.
*
* @param image the base image
* @return the combined image
*/
@NonNull
public Image addErrorIcon(@NonNull Image image) {
if (mErrorIcons != null) {
Image combined = mErrorIcons.get(image);
if (combined != null) {
return combined;
}
} else {
mErrorIcons = new IdentityHashMap<Image, Image>();
}
Image combined = new ErrorImageComposite(image, false).createImage();
mErrorIcons.put(image, combined);
return combined;
}
/**
* Returns an image with a warning icon overlaid on it. The icons are
* cached, so the base image should be cached as well, or this method will
* keep storing new overlays into its cache.
*
* @param image the base image
* @return the combined image
*/
@NonNull
public Image addWarningIcon(@NonNull Image image) {
if (mWarningIcons != null) {
Image combined = mWarningIcons.get(image);
if (combined != null) {
return combined;
}
} else {
mWarningIcons = new IdentityHashMap<Image, Image>();
}
Image combined = new ErrorImageComposite(image, true).createImage();
mWarningIcons.put(image, combined);
return combined;
}
/**
* A simple image description that generates a 16x16 image which consists
* of a colored letter inside a black & white circle.
*/
private static class LetterImageDescriptor extends ImageDescriptor {
private final char mLetter;
private final int mColor;
private final int mShape;
public LetterImageDescriptor(char letter, int color, int shape) {
mLetter = Character.toUpperCase(letter);
mColor = color;
mShape = shape;
}
@Override
public ImageData getImageData() {
final int SX = 15;
final int SY = 15;
final int RX = 4;
final int RY = 4;
Display display = Display.getCurrent();
if (display == null) {
return null;
}
Image image = new Image(display, SX, SY);
GC gc = new GC(image);
gc.setAdvanced(true);
gc.setAntialias(SWT.ON);
gc.setTextAntialias(SWT.ON);
// image.setBackground() does not appear to have any effect; we must explicitly
// paint into the image the background color we want masked out later.
// HOWEVER, alpha transparency does not work; we only get to mark a single color
// as transparent. You might think we could pick a system color (to avoid having
// to allocate and dispose the color), or a wildly unique color (to make sure we
// don't accidentally pick up any extra pixels in the image as transparent), but
// this has the very unfortunate side effect of making neighbor pixels in the
// antialiased rendering of the circle pick up shades of that alternate color,
// which looks bad. Therefore we pick a color which is similar to one of our
// existing colors but hopefully different from most pixels. A visual check
// confirms that this seems to work pretty well:
RGB backgroundRgb = new RGB(254, 254, 254);
Color backgroundColor = new Color(display, backgroundRgb);
gc.setBackground(backgroundColor);
gc.fillRectangle(0, 0, SX, SY);
gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE));
if (mShape == SHAPE_CIRCLE) {
gc.fillOval(0, 0, SX - 1, SY - 1);
} else if (mShape == SHAPE_RECT) {
gc.fillRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY);
}
gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK));
gc.setLineWidth(1);
if (mShape == SHAPE_CIRCLE) {
gc.drawOval(0, 0, SX - 1, SY - 1);
} else if (mShape == SHAPE_RECT) {
gc.drawRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY);
}
// Get a bold version of the default system font, if possible.
Font font = display.getSystemFont();
FontData[] fds = font.getFontData();
fds[0].setStyle(SWT.BOLD);
// use 3/4th of the circle diameter for the font size (in pixels)
// and convert it to "font points" (font points in SWT are hardcoded in an
// arbitrary 72 dpi and then converted in real pixels using whatever is
// indicated by getDPI -- at least that's how it works under Win32).
fds[0].setHeight((int) ((SY + 1) * 3./4. * 72./display.getDPI().y));
// Note: win32 implementation always uses fds[0] so we change just that one.
// getFontData indicates that the array of fd is really an unusual thing for X11.
font = new Font(display, fds);
gc.setFont(font);
gc.setForeground(display.getSystemColor(mColor));
// Text measurement varies so slightly depending on the platform
int ofx = 0;
int ofy = 0;
if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
ofx = +1;
ofy = -1;
} else if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) {
// Tweak pixel positioning of some letters that don't look good on the Mac
if (mLetter != 'T' && mLetter != 'V') {
ofy = -1;
}
if (mLetter == 'I') {
ofx = -2;
}
}
String s = Character.toString(mLetter);
Point p = gc.textExtent(s);
int tx = (SX + ofx - p.x) / 2;
int ty = (SY + ofy - p.y) / 2;
gc.drawText(s, tx, ty, true /* isTransparent */);
font.dispose();
gc.dispose();
ImageData data = image.getImageData();
image.dispose();
backgroundColor.dispose();
// Set transparent pixel in the palette such that on paint (over palette,
// which has a background of SWT.COLOR_WIDGET_BACKGROUND, and over the tree
// which has a white background) we will substitute the background in for
// the backgroundPixel.
int backgroundPixel = data.palette.getPixel(backgroundRgb);
data.transparentPixel = backgroundPixel;
return data;
}
}
}