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