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