Home | History | Annotate | Download | only in editors
      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 
     66     private IconFactory() {
     67     }
     68 
     69     public static synchronized IconFactory getInstance() {
     70         if (sInstance == null) {
     71             sInstance = new IconFactory();
     72         }
     73         return sInstance;
     74     }
     75 
     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      */
    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      */
    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      */
    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      */
    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
    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
    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      */
    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
    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
    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 
    337         public LetterImageDescriptor(char letter, int color, int shape) {
    338             mLetter = Character.toUpperCase(letter);
    339             mColor = color;
    340             mShape = shape;
    341         }
    342 
    343         @Override
    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