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 
     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