37948dfd49d8417684a3ed443eed6e61103053d2
[idea/community.git] / platform / util / src / com / intellij / openapi / util / IconLoader.java
1 /*
2  * Copyright 2000-2014 JetBrains s.r.o.
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 package com.intellij.openapi.util;
17
18 import com.intellij.openapi.diagnostic.Logger;
19 import com.intellij.openapi.util.text.StringUtil;
20 import com.intellij.reference.SoftReference;
21 import com.intellij.util.ConcurrencyUtil;
22 import com.intellij.util.ImageLoader;
23 import com.intellij.util.ReflectionUtil;
24 import com.intellij.util.RetinaImage;
25 import com.intellij.util.containers.ContainerUtil;
26 import com.intellij.util.containers.WeakHashMap;
27 import com.intellij.util.ui.JBImageIcon;
28 import com.intellij.util.ui.UIUtil;
29 import org.jetbrains.annotations.NonNls;
30 import org.jetbrains.annotations.NotNull;
31 import org.jetbrains.annotations.Nullable;
32
33 import javax.swing.*;
34 import java.awt.*;
35 import java.awt.image.BufferedImage;
36 import java.awt.image.FilteredImageSource;
37 import java.awt.image.ImageProducer;
38 import java.lang.ref.Reference;
39 import java.lang.reflect.Field;
40 import java.net.URL;
41 import java.util.HashMap;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.concurrent.ConcurrentMap;
45
46 public final class IconLoader {
47   private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.IconLoader");
48   public static boolean STRICT = false;
49   private static boolean USE_DARK_ICONS = UIUtil.isUnderDarcula();
50
51   @SuppressWarnings("MismatchedQueryAndUpdateOfCollection")
52   private static final ConcurrentMap<URL, CachedImageIcon> ourIconsCache = ContainerUtil.newConcurrentMap(100, 0.9f, 2);
53
54   /**
55    * This cache contains mapping between icons and disabled icons.
56    */
57   private static final Map<Icon, Icon> ourIcon2DisabledIcon = new WeakHashMap<Icon, Icon>(200);
58   @NonNls private static final Map<String, String> ourDeprecatedIconsReplacements = new HashMap<String, String>();
59
60   static {
61     ourDeprecatedIconsReplacements.put("/general/toolWindowDebugger.png", "AllIcons.Toolwindows.ToolWindowDebugger");
62     ourDeprecatedIconsReplacements.put("/general/toolWindowChanges.png", "AllIcons.Toolwindows.ToolWindowChanges");
63
64     ourDeprecatedIconsReplacements.put("/actions/showSettings.png", "AllIcons.General.ProjectSettings");
65     ourDeprecatedIconsReplacements.put("/general/ideOptions.png", "AllIcons.General.Settings");
66     ourDeprecatedIconsReplacements.put("/general/applicationSettings.png", "AllIcons.General.Settings");
67     ourDeprecatedIconsReplacements.put("/toolbarDecorator/add.png", "AllIcons.General.Add");
68     ourDeprecatedIconsReplacements.put("/vcs/customizeView.png", "AllIcons.General.Settings");
69
70     ourDeprecatedIconsReplacements.put("/vcs/refresh.png", "AllIcons.Actions.Refresh");
71     ourDeprecatedIconsReplacements.put("/actions/sync.png", "AllIcons.Actions.Refresh");
72     ourDeprecatedIconsReplacements.put("/actions/refreshUsages.png", "AllIcons.Actions.Rerun");
73
74     ourDeprecatedIconsReplacements.put("/compiler/error.png", "AllIcons.General.Error");
75     ourDeprecatedIconsReplacements.put("/compiler/hideWarnings.png", "AllIcons.General.HideWarnings");
76     ourDeprecatedIconsReplacements.put("/compiler/information.png", "AllIcons.General.Information");
77     ourDeprecatedIconsReplacements.put("/compiler/warning.png", "AllIcons.General.Warning");
78     ourDeprecatedIconsReplacements.put("/ide/errorSign.png", "AllIcons.General.Error");
79
80     ourDeprecatedIconsReplacements.put("/ant/filter.png", "AllIcons.General.Filter");
81     ourDeprecatedIconsReplacements.put("/inspector/useFilter.png", "AllIcons.General.Filter");
82
83     ourDeprecatedIconsReplacements.put("/actions/showSource.png", "AllIcons.Actions.Preview");
84     ourDeprecatedIconsReplacements.put("/actions/consoleHistory.png", "AllIcons.General.MessageHistory");
85     ourDeprecatedIconsReplacements.put("/vcs/messageHistory.png", "AllIcons.General.MessageHistory");
86   }
87
88   private static final ImageIcon EMPTY_ICON = new ImageIcon(UIUtil.createImage(1, 1, BufferedImage.TYPE_3BYTE_BGR)) {
89     @NonNls
90     public String toString() {
91       return "Empty icon " + super.toString();
92     }
93   };
94
95   private static boolean ourIsActivated = false;
96
97   private IconLoader() { }
98
99   @Deprecated
100   public static Icon getIcon(@NotNull final Image image) {
101     return new JBImageIcon(image);
102   }
103
104   public static void setUseDarkIcons(boolean useDarkIcons) {
105     USE_DARK_ICONS = useDarkIcons;
106     ourIconsCache.clear();
107     ourIcon2DisabledIcon.clear();
108   }
109
110
111   //TODO[kb] support iconsets
112   //public static Icon getIcon(@NotNull final String path, @NotNull final String darkVariantPath) {
113   //  return new InvariantIcon(getIcon(path), getIcon(darkVariantPath));
114   //}
115
116   @NotNull
117   public static Icon getIcon(@NonNls @NotNull final String path) {
118     Class callerClass = ReflectionUtil.getGrandCallerClass();
119
120     assert callerClass != null : path;
121     return getIcon(path, callerClass);
122   }
123
124   @Nullable
125   private static Icon getReflectiveIcon(@NotNull String path, ClassLoader classLoader) {
126     try {
127       @NonNls String pckg = path.startsWith("AllIcons.") ? "com.intellij.icons." : "icons.";
128       Class cur = Class.forName(pckg + path.substring(0, path.lastIndexOf('.')).replace('.', '$'), true, classLoader);
129       Field field = cur.getField(path.substring(path.lastIndexOf('.') + 1));
130
131       return (Icon)field.get(null);
132     }
133     catch (Exception e) {
134       return null;
135     }
136   }
137
138   @Nullable
139   /**
140    * Might return null if icon was not found.
141    * Use only if you expected null return value, otherwise see {@link IconLoader#getIcon(java.lang.String)}
142    */
143   public static Icon findIcon(@NonNls @NotNull String path) {
144     Class callerClass = ReflectionUtil.getGrandCallerClass();
145     if (callerClass == null) return null;
146     return findIcon(path, callerClass);
147   }
148
149   @NotNull
150   public static Icon getIcon(@NotNull String path, @NotNull final Class aClass) {
151     final Icon icon = findIcon(path, aClass);
152     if (icon == null) {
153       LOG.error("Icon cannot be found in '" + path + "', aClass='" + aClass + "'");
154     }
155     return icon;
156   }
157
158   public static void activate() {
159     ourIsActivated = true;
160   }
161
162   public static void deactivate() {
163     ourIsActivated = false;
164   }
165
166   private static boolean isLoaderDisabled() {
167     return !ourIsActivated;
168   }
169
170   /**
171    * Might return null if icon was not found.
172    * Use only if you expected null return value, otherwise see {@link IconLoader#getIcon(java.lang.String, java.lang.Class)}
173    */
174   @Nullable
175   public static Icon findIcon(@NotNull final String path, @NotNull final Class aClass) {
176     return findIcon(path, aClass, false);
177   }
178
179   @Nullable
180   public static Icon findIcon(@NotNull String path, @NotNull final Class aClass, boolean computeNow) {
181     path = undeprecate(path);
182     if (isReflectivePath(path)) return getReflectiveIcon(path, aClass.getClassLoader());
183
184     URL myURL = aClass.getResource(path);
185     if (myURL == null) {
186       if (STRICT) throw new RuntimeException("Can't find icon in '" + path + "' near "+aClass);
187       return null;
188     }
189     return findIcon(myURL);
190   }
191
192   @NotNull
193   private static String undeprecate(@NotNull String path) {
194     String replacement = ourDeprecatedIconsReplacements.get(path);
195     return replacement == null ? path : replacement;
196   }
197
198   private static boolean isReflectivePath(@NotNull String path) {
199     List<String> paths = StringUtil.split(path, ".");
200     return paths.size() > 1 && paths.get(0).endsWith("Icons");
201   }
202
203   @Nullable
204   public static Icon findIcon(URL url) {
205     return findIcon(url, true);
206   }
207
208   @Nullable
209   public static Icon findIcon(URL url, boolean useCache) {
210     if (url == null) {
211       return null;
212     }
213     CachedImageIcon icon = ourIconsCache.get(url);
214     if (icon == null) {
215       icon = new CachedImageIcon(url);
216       if (useCache) {
217         icon = ConcurrencyUtil.cacheOrGet(ourIconsCache, url, icon);
218       }
219     }
220     return icon;
221   }
222
223   @Nullable
224   public static Icon findIcon(@NotNull String path, @NotNull ClassLoader classLoader) {
225     path = undeprecate(path);
226     if (isReflectivePath(path)) return getReflectiveIcon(path, classLoader);
227     if (!StringUtil.startsWithChar(path, '/')) return null;
228
229     final URL url = classLoader.getResource(path.substring(1));
230     return findIcon(url);
231   }
232
233   @Nullable
234   private static Icon checkIcon(final Image image, @NotNull URL url) {
235     if (image == null || image.getHeight(LabelHolder.ourFakeComponent) < 1) { // image wasn't loaded or broken
236       return null;
237     }
238
239     final Icon icon = getIcon(image);
240     if (icon != null && !isGoodSize(icon)) {
241       LOG.error("Invalid icon: " + url); // # 22481
242       return EMPTY_ICON;
243     }
244     return icon;
245   }
246
247   public static boolean isGoodSize(@NotNull final Icon icon) {
248     return icon.getIconWidth() > 0 && icon.getIconHeight() > 0;
249   }
250
251   /**
252    * Gets (creates if necessary) disabled icon based on the passed one.
253    *
254    * @return <code>ImageIcon</code> constructed from disabled image of passed icon.
255    */
256   @Nullable
257   public static Icon getDisabledIcon(Icon icon) {
258     if (icon instanceof LazyIcon) icon = ((LazyIcon)icon).getOrComputeIcon();
259     if (icon == null) return null;
260
261     Icon disabledIcon = ourIcon2DisabledIcon.get(icon);
262     if (disabledIcon == null) {
263       if (!isGoodSize(icon)) {
264         LOG.error(icon); // # 22481
265         return EMPTY_ICON;
266       }
267       final int scale = UIUtil.isRetina() ? 2 : 1;
268       @SuppressWarnings("UndesirableClassUsage")
269       BufferedImage image = new BufferedImage(scale*icon.getIconWidth(), scale*icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB);
270       final Graphics2D graphics = image.createGraphics();
271
272       graphics.setColor(UIUtil.TRANSPARENT_COLOR);
273       graphics.fillRect(0, 0, icon.getIconWidth(), icon.getIconHeight());
274       graphics.scale(scale, scale);
275       icon.paintIcon(LabelHolder.ourFakeComponent, graphics, 0, 0);
276
277       graphics.dispose();
278
279       Image img = createDisabled(image);
280       if (UIUtil.isRetina()) img = RetinaImage.createFrom(img, 2, ImageLoader.ourComponent);
281
282       disabledIcon = new JBImageIcon(img);
283       ourIcon2DisabledIcon.put(icon, disabledIcon);
284     }
285     return disabledIcon;
286   }
287
288   private static Image createDisabled(BufferedImage image) {
289     final GrayFilter filter = UIUtil.getGrayFilter();
290     final ImageProducer prod = new FilteredImageSource(image.getSource(), filter);
291     return Toolkit.getDefaultToolkit().createImage(prod);
292   }
293
294   public static Icon getTransparentIcon(@NotNull final Icon icon) {
295     return getTransparentIcon(icon, 0.5f);
296   }
297
298   public static Icon getTransparentIcon(@NotNull final Icon icon, final float alpha) {
299     return new Icon() {
300       @Override
301       public int getIconHeight() {
302         return icon.getIconHeight();
303       }
304
305       @Override
306       public int getIconWidth() {
307         return icon.getIconWidth();
308       }
309
310       @Override
311       public void paintIcon(final Component c, final Graphics g, final int x, final int y) {
312         final Graphics2D g2 = (Graphics2D)g;
313         final Composite saveComposite = g2.getComposite();
314         g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha));
315         icon.paintIcon(c, g2, x, y);
316         g2.setComposite(saveComposite);
317       }
318     };
319   }
320
321   private static final class CachedImageIcon implements Icon {
322     private Object myRealIcon;
323     @NotNull
324     private final URL myUrl;
325     private boolean dark;
326
327     public CachedImageIcon(@NotNull URL url) {
328       myUrl = url;
329       dark = USE_DARK_ICONS;
330     }
331
332     @NotNull
333     private synchronized Icon getRealIcon() {
334       if (isLoaderDisabled() && (myRealIcon == null || dark != USE_DARK_ICONS)) return EMPTY_ICON;
335
336       if (dark != USE_DARK_ICONS) {
337         myRealIcon = null;
338         dark = USE_DARK_ICONS;
339       }
340       Object realIcon = myRealIcon;
341       if (realIcon instanceof Icon) return (Icon)realIcon;
342
343       Icon icon;
344       if (realIcon instanceof Reference) {
345         icon = ((Reference<Icon>)realIcon).get();
346         if (icon != null) return icon;
347       }
348
349       Image image = ImageLoader.loadFromUrl(myUrl);
350       icon = checkIcon(image, myUrl);
351
352       if (icon != null) {
353         if (icon.getIconWidth() < 50 && icon.getIconHeight() < 50) {
354           realIcon = icon;
355         }
356         else {
357           realIcon = new SoftReference<Icon>(icon);
358         }
359         myRealIcon = realIcon;
360       }
361
362       return icon == null ? EMPTY_ICON : icon;
363     }
364
365     @Override
366     public void paintIcon(Component c, Graphics g, int x, int y) {
367       getRealIcon().paintIcon(c, g, x, y);
368     }
369
370     @Override
371     public int getIconWidth() {
372       return getRealIcon().getIconWidth();
373     }
374
375     @Override
376     public int getIconHeight() {
377       return getRealIcon().getIconHeight();
378     }
379
380     @Override
381     public String toString() {
382       return myUrl.toString();
383     }
384   }
385
386   public abstract static class LazyIcon implements Icon {
387     private boolean myWasComputed;
388     private Icon myIcon;
389     private boolean isDarkVariant = USE_DARK_ICONS;
390
391     @Override
392     public void paintIcon(Component c, Graphics g, int x, int y) {
393       final Icon icon = getOrComputeIcon();
394       if (icon != null) {
395         icon.paintIcon(c, g, x, y);
396       }
397     }
398
399     @Override
400     public int getIconWidth() {
401       final Icon icon = getOrComputeIcon();
402       return icon != null ? icon.getIconWidth() : 0;
403     }
404
405     @Override
406     public int getIconHeight() {
407       final Icon icon = getOrComputeIcon();
408       return icon != null ? icon.getIconHeight() : 0;
409     }
410
411     protected final synchronized Icon getOrComputeIcon() {
412       if (!myWasComputed || isDarkVariant != USE_DARK_ICONS) {
413         isDarkVariant = USE_DARK_ICONS;
414         myWasComputed = true;
415         myIcon = compute();
416       }
417
418       return myIcon;
419     }
420
421     public final void load() {
422       getIconWidth();
423     }
424
425     protected abstract Icon compute();
426   }
427
428   private static class LabelHolder {
429     /**
430      * To get disabled icon with paint it into the image. Some icons require
431      * not null component to paint.
432      */
433     private static final JComponent ourFakeComponent = new JLabel();
434   }
435 }