1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 18 package com.android.ide.eclipse.adt.internal.editors; 19 20 import com.android.annotations.NonNull; 21 import com.android.annotations.Nullable; 22 import com.android.ide.eclipse.adt.AdtPlugin; 23 import com.android.sdklib.SdkConstants; 24 25 import org.eclipse.jface.resource.ImageDescriptor; 26 import org.eclipse.swt.SWT; 27 import org.eclipse.swt.graphics.Color; 28 import org.eclipse.swt.graphics.Font; 29 import org.eclipse.swt.graphics.FontData; 30 import org.eclipse.swt.graphics.GC; 31 import org.eclipse.swt.graphics.Image; 32 import org.eclipse.swt.graphics.ImageData; 33 import org.eclipse.swt.graphics.Point; 34 import org.eclipse.swt.graphics.RGB; 35 import org.eclipse.swt.widgets.Display; 36 import org.eclipse.ui.plugin.AbstractUIPlugin; 37 38 import java.net.URL; 39 import java.util.HashMap; 40 41 /** 42 * Factory to generate icons for Android Editors. 43 * <p/> 44 * Icons are kept here and reused. 45 */ 46 public class IconFactory { 47 public static final int COLOR_RED = SWT.COLOR_DARK_RED; 48 public static final int COLOR_GREEN = SWT.COLOR_DARK_GREEN; 49 public static final int COLOR_BLUE = SWT.COLOR_DARK_BLUE; 50 public static final int COLOR_DEFAULT = SWT.COLOR_BLACK; 51 52 public static final int SHAPE_CIRCLE = 'C'; 53 public static final int SHAPE_RECT = 'R'; 54 public static final int SHAPE_DEFAULT = SHAPE_CIRCLE; 55 56 private static IconFactory sInstance; 57 58 private HashMap<String, Image> mIconMap = new HashMap<String, Image>(); 59 private HashMap<URL, Image> mUrlMap = new HashMap<URL, Image>(); 60 private HashMap<String, ImageDescriptor> mImageDescMap = new HashMap<String, ImageDescriptor>(); 61 62 private IconFactory() { 63 } 64 65 public static synchronized IconFactory getInstance() { 66 if (sInstance == null) { 67 sInstance = new IconFactory(); 68 } 69 return sInstance; 70 } 71 72 public void dispose() { 73 // Dispose icons 74 for (Image icon : mIconMap.values()) { 75 // The map can contain null values 76 if (icon != null) { 77 icon.dispose(); 78 } 79 } 80 mIconMap.clear(); 81 for (Image icon : mUrlMap.values()) { 82 // The map can contain null values 83 if (icon != null) { 84 icon.dispose(); 85 } 86 } 87 mUrlMap.clear(); 88 } 89 90 /** 91 * Returns an Image for a given icon name. 92 * <p/> 93 * Callers should not dispose it. 94 * 95 * @param osName The leaf name, without the extension, of an existing icon in the 96 * editor's "icons" directory. If it doesn't exists, a default icon will be 97 * generated automatically based on the name. 98 */ 99 public Image getIcon(String osName) { 100 return getIcon(osName, COLOR_DEFAULT, SHAPE_DEFAULT); 101 } 102 103 /** 104 * Returns an Image for a given icon name. 105 * <p/> 106 * Callers should not dispose it. 107 * 108 * @param osName The leaf name, without the extension, of an existing icon in the 109 * editor's "icons" directory. If it doesn't exist, a default icon will be 110 * generated automatically based on the name. 111 * @param color The color of the text in the automatically generated icons, 112 * one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED. 113 * @param shape The shape of the icon in the automatically generated icons, 114 * one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT. 115 */ 116 public Image getIcon(String osName, int color, int shape) { 117 String key = Character.toString((char) shape) + Integer.toString(color) + osName; 118 Image icon = mIconMap.get(key); 119 if (icon == null && !mIconMap.containsKey(key)) { 120 ImageDescriptor id = getImageDescriptor(osName, color, shape); 121 if (id != null) { 122 icon = id.createImage(); 123 } 124 // Note that we store null references in the icon map, to avoid looking them 125 // up every time. If it didn't exist once, it will not exist later. 126 mIconMap.put(key, icon); 127 } 128 return icon; 129 } 130 131 /** 132 * Returns an ImageDescriptor for a given icon name. 133 * <p/> 134 * Callers should not dispose it. 135 * 136 * @param osName The leaf name, without the extension, of an existing icon in the 137 * editor's "icons" directory. If it doesn't exists, a default icon will be 138 * generated automatically based on the name. 139 */ 140 public ImageDescriptor getImageDescriptor(String osName) { 141 return getImageDescriptor(osName, COLOR_DEFAULT, SHAPE_DEFAULT); 142 } 143 144 /** 145 * Returns an ImageDescriptor for a given icon name. 146 * <p/> 147 * Callers should not dispose it. 148 * 149 * @param osName The leaf name, without the extension, of an existing icon in the 150 * editor's "icons" directory. If it doesn't exists, a default icon will be 151 * generated automatically based on the name. 152 * @param color The color of the text in the automatically generated icons. 153 * one of COLOR_DEFAULT, COLOR_RED, COLOR_BLUE or COLOR_RED. 154 * @param shape The shape of the icon in the automatically generated icons, 155 * one of SHAPE_DEFAULT, SHAPE_CIRCLE or SHAPE_RECT. 156 */ 157 public ImageDescriptor getImageDescriptor(String osName, int color, int shape) { 158 String key = Character.toString((char) shape) + Integer.toString(color) + osName; 159 ImageDescriptor id = mImageDescMap.get(key); 160 if (id == null && !mImageDescMap.containsKey(key)) { 161 id = AbstractUIPlugin.imageDescriptorFromPlugin( 162 AdtPlugin.PLUGIN_ID, 163 String.format("/icons/%1$s.png", osName)); //$NON-NLS-1$ 164 165 if (id == null) { 166 id = new LetterImageDescriptor(osName.charAt(0), color, shape); 167 } 168 169 // Note that we store null references in the icon map, to avoid looking them 170 // up every time. If it didn't exist once, it will not exist later. 171 mImageDescMap.put(key, id); 172 } 173 return id; 174 } 175 176 /** 177 * Returns an Image for a given icon name. 178 * <p/> 179 * Callers should not dispose it. 180 * 181 * @param osName The leaf name, without the extension, of an existing icon 182 * in the editor's "icons" directory. If it doesn't exist, the 183 * fallback will be used instead. 184 * @param fallback the fallback icon name to use if the primary icon does 185 * not exist, or null if the method should return null if the 186 * image does not exist 187 * @return the icon, which should not be disposed by the caller, or null 188 * if the image does not exist *and* 189 */ 190 @Nullable 191 public Image getIcon(@NonNull String osName, @Nullable String fallback) { 192 String key = osName; 193 Image icon = mIconMap.get(key); 194 if (icon == null && !mIconMap.containsKey(key)) { 195 ImageDescriptor id = getImageDescriptor(osName, fallback); 196 if (id != null) { 197 icon = id.createImage(); 198 } 199 // Note that we store null references in the icon map, to avoid looking them 200 // up every time. If it didn't exist once, it will not exist later. 201 mIconMap.put(key, icon); 202 } 203 return icon; 204 } 205 206 /** 207 * Returns an icon of the given name, or if that image does not exist and 208 * icon of the given fallback name. 209 * 210 * @param key the icon name 211 * @param fallbackKey the fallback image to use if the primary key does not 212 * exist 213 * @return the image descriptor, or null if the image does not exist and the 214 * fallbackKey is null 215 */ 216 @Nullable 217 public ImageDescriptor getImageDescriptor(@NonNull String key, @Nullable String fallbackKey) { 218 ImageDescriptor id = mImageDescMap.get(key); 219 if (id == null && !mImageDescMap.containsKey(key)) { 220 id = AbstractUIPlugin.imageDescriptorFromPlugin( 221 AdtPlugin.PLUGIN_ID, 222 String.format("/icons/%1$s.png", key)); //$NON-NLS-1$ 223 if (id == null) { 224 if (fallbackKey == null) { 225 return null; 226 } 227 id = getImageDescriptor(fallbackKey); 228 } 229 230 // Place the fallback image for this key as well such that we don't keep trying 231 // to load the failed image 232 mImageDescMap.put(key, id); 233 } 234 235 return id; 236 } 237 238 /** 239 * Returns the image indicated by the given URL 240 * 241 * @param url the url to the image resources 242 * @return the image for the url, or null if it cannot be initialized 243 */ 244 public Image getIcon(URL url) { 245 Image image = mUrlMap.get(url); 246 if (image == null) { 247 ImageDescriptor descriptor = ImageDescriptor.createFromURL(url); 248 image = descriptor.createImage(); 249 mUrlMap.put(url, image); 250 } 251 252 return image; 253 } 254 255 /** 256 * A simple image description that generates a 16x16 image which consists 257 * of a colored letter inside a black & white circle. 258 */ 259 private static class LetterImageDescriptor extends ImageDescriptor { 260 261 private final char mLetter; 262 private final int mColor; 263 private final int mShape; 264 265 public LetterImageDescriptor(char letter, int color, int shape) { 266 mLetter = Character.toUpperCase(letter); 267 mColor = color; 268 mShape = shape; 269 } 270 271 @Override 272 public ImageData getImageData() { 273 274 final int SX = 15; 275 final int SY = 15; 276 final int RX = 4; 277 final int RY = 4; 278 279 Display display = Display.getCurrent(); 280 if (display == null) { 281 return null; 282 } 283 284 Image image = new Image(display, SX, SY); 285 286 GC gc = new GC(image); 287 gc.setAdvanced(true); 288 gc.setAntialias(SWT.ON); 289 gc.setTextAntialias(SWT.ON); 290 291 // image.setBackground() does not appear to have any effect; we must explicitly 292 // paint into the image the background color we want masked out later. 293 // HOWEVER, alpha transparency does not work; we only get to mark a single color 294 // as transparent. You might think we could pick a system color (to avoid having 295 // to allocate and dispose the color), or a wildly unique color (to make sure we 296 // don't accidentally pick up any extra pixels in the image as transparent), but 297 // this has the very unfortunate side effect of making neighbor pixels in the 298 // antialiased rendering of the circle pick up shades of that alternate color, 299 // which looks bad. Therefore we pick a color which is similar to one of our 300 // existing colors but hopefully different from most pixels. A visual check 301 // confirms that this seems to work pretty well: 302 RGB backgroundRgb = new RGB(254, 254, 254); 303 Color backgroundColor = new Color(display, backgroundRgb); 304 gc.setBackground(backgroundColor); 305 gc.fillRectangle(0, 0, SX, SY); 306 307 gc.setBackground(display.getSystemColor(SWT.COLOR_WHITE)); 308 if (mShape == SHAPE_CIRCLE) { 309 gc.fillOval(0, 0, SX - 1, SY - 1); 310 } else if (mShape == SHAPE_RECT) { 311 gc.fillRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY); 312 } 313 314 gc.setForeground(display.getSystemColor(SWT.COLOR_BLACK)); 315 gc.setLineWidth(1); 316 if (mShape == SHAPE_CIRCLE) { 317 gc.drawOval(0, 0, SX - 1, SY - 1); 318 } else if (mShape == SHAPE_RECT) { 319 gc.drawRoundRectangle(0, 0, SX - 1, SY - 1, RX, RY); 320 } 321 322 // Get a bold version of the default system font, if possible. 323 Font font = display.getSystemFont(); 324 FontData[] fds = font.getFontData(); 325 fds[0].setStyle(SWT.BOLD); 326 // use 3/4th of the circle diameter for the font size (in pixels) 327 // and convert it to "font points" (font points in SWT are hardcoded in an 328 // arbitrary 72 dpi and then converted in real pixels using whatever is 329 // indicated by getDPI -- at least that's how it works under Win32). 330 fds[0].setHeight((int) ((SY + 1) * 3./4. * 72./display.getDPI().y)); 331 // Note: win32 implementation always uses fds[0] so we change just that one. 332 // getFontData indicates that the array of fd is really an unusual thing for X11. 333 font = new Font(display, fds); 334 gc.setFont(font); 335 gc.setForeground(display.getSystemColor(mColor)); 336 337 // Text measurement varies so slightly depending on the platform 338 int ofx = 0; 339 int ofy = 0; 340 if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) { 341 ofx = +1; 342 ofy = -1; 343 } else if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_DARWIN) { 344 // Tweak pixel positioning of some letters that don't look good on the Mac 345 if (mLetter != 'T' && mLetter != 'V') { 346 ofy = -1; 347 } 348 if (mLetter == 'I') { 349 ofx = -2; 350 } 351 } 352 353 String s = Character.toString(mLetter); 354 Point p = gc.textExtent(s); 355 int tx = (SX + ofx - p.x) / 2; 356 int ty = (SY + ofy - p.y) / 2; 357 gc.drawText(s, tx, ty, true /* isTransparent */); 358 359 font.dispose(); 360 gc.dispose(); 361 362 ImageData data = image.getImageData(); 363 image.dispose(); 364 backgroundColor.dispose(); 365 366 // Set transparent pixel in the palette such that on paint (over palette, 367 // which has a background of SWT.COLOR_WIDGET_BACKGROUND, and over the tree 368 // which has a white background) we will substitute the background in for 369 // the backgroundPixel. 370 int backgroundPixel = data.palette.getPixel(backgroundRgb); 371 data.transparentPixel = backgroundPixel; 372 373 return data; 374 } 375 } 376 } 377