You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
557 lines
17 KiB
557 lines
17 KiB
/* |
|
* Copyright 2000-2014 JetBrains s.r.o. |
|
* |
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
* you may not use this file except in compliance with the License. |
|
* You may obtain a copy of the License at |
|
* |
|
* http://www.apache.org/licenses/LICENSE-2.0 |
|
* |
|
* Unless required by applicable law or agreed to in writing, software |
|
* distributed under the License is distributed on an "AS IS" BASIS, |
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
* See the License for the specific language governing permissions and |
|
* limitations under the License. |
|
*/ |
|
|
|
package com.bulenkov.iconloader; |
|
|
|
import com.bulenkov.iconloader.util.*; |
|
import org.jetbrains.annotations.NonNls; |
|
import org.jetbrains.annotations.NotNull; |
|
import org.jetbrains.annotations.Nullable; |
|
|
|
import javax.swing.*; |
|
import java.awt.*; |
|
import java.awt.image.BufferedImage; |
|
import java.awt.image.ImageFilter; |
|
import java.lang.ref.Reference; |
|
import java.lang.reflect.Field; |
|
import java.net.URL; |
|
import java.util.*; |
|
import java.util.List; |
|
import java.util.concurrent.ConcurrentHashMap; |
|
import java.util.concurrent.ConcurrentMap; |
|
import java.util.concurrent.atomic.AtomicBoolean; |
|
|
|
/** |
|
* @author Konstantin Bulenkov |
|
*/ |
|
@SuppressWarnings("UnusedDeclaration") |
|
public final class IconLoader { |
|
public static boolean STRICT = false; |
|
private static boolean USE_DARK_ICONS = UIUtil.isUnderDarcula(); |
|
|
|
@SuppressWarnings("MismatchedQueryAndUpdateOfCollection") |
|
private static final ConcurrentMap<URL, CachedImageIcon> ourIconsCache = new ConcurrentHashMap<URL, CachedImageIcon>(100, 0.9f, 2); |
|
|
|
/** |
|
* This cache contains mapping between icons and disabled icons. |
|
*/ |
|
private static final Map<Icon, Icon> ourIcon2DisabledIcon = new WeakHashMap<Icon, Icon>(200); |
|
|
|
private static float SCALE = JBUI.scale(1f); |
|
private static ImageFilter IMAGE_FILTER; |
|
|
|
private static final ImageIcon EMPTY_ICON = new ImageIcon(UIUtil.createImage(1, 1, BufferedImage.TYPE_3BYTE_BGR)) { |
|
@NonNls |
|
public String toString() { |
|
return "Empty icon " + super.toString(); |
|
} |
|
}; |
|
|
|
private static AtomicBoolean ourIsActivated = new AtomicBoolean(true); |
|
private static AtomicBoolean ourIsSaveRealIconPath = new AtomicBoolean(false); |
|
public static final Component ourComponent = new Component() {}; |
|
|
|
private IconLoader() { } |
|
|
|
@Deprecated |
|
public static Icon getIcon(@NotNull final Image image) { |
|
return new JBImageIcon(image); |
|
} |
|
|
|
public static void setUseDarkIcons(boolean useDarkIcons) { |
|
USE_DARK_ICONS = useDarkIcons; |
|
clearCache(); |
|
} |
|
|
|
public static void setScale(float scale) { |
|
if (scale != SCALE) { |
|
SCALE = scale; |
|
clearCache(); |
|
} |
|
} |
|
|
|
public static void setFilter(ImageFilter filter) { |
|
if (!Registry.is("color.blindness.icon.filter")) { |
|
filter = null; |
|
} |
|
if (IMAGE_FILTER != filter) { |
|
IMAGE_FILTER = filter; |
|
clearCache(); |
|
} |
|
} |
|
|
|
private static void clearCache() { |
|
ourIconsCache.clear(); |
|
ourIcon2DisabledIcon.clear(); |
|
} |
|
|
|
//TODO[kb] support iconsets |
|
//public static Icon getIcon(@NotNull final String path, @NotNull final String darkVariantPath) { |
|
// return new InvariantIcon(getIcon(path), getIcon(darkVariantPath)); |
|
//} |
|
|
|
@NotNull |
|
public static Icon getIcon(@NonNls @NotNull final String path) { |
|
Class callerClass = ReflectionUtil.getGrandCallerClass(); |
|
|
|
assert callerClass != null : path; |
|
return getIcon(path, callerClass); |
|
} |
|
|
|
@Nullable |
|
private static Icon getReflectiveIcon(@NotNull String path, ClassLoader classLoader) { |
|
try { |
|
@NonNls String pckg = path.startsWith("AllIcons.") ? "com.intellij.icons." : "icons."; |
|
Class cur = Class.forName(pckg + path.substring(0, path.lastIndexOf('.')).replace('.', '$'), true, classLoader); |
|
Field field = cur.getField(path.substring(path.lastIndexOf('.') + 1)); |
|
|
|
return (Icon)field.get(null); |
|
} |
|
catch (Exception e) { |
|
return null; |
|
} |
|
} |
|
|
|
/** |
|
* Might return null if icon was not found. |
|
* Use only if you expected null return value, otherwise see {@link IconLoader#getIcon(String)} |
|
*/ |
|
@Nullable |
|
public static Icon findIcon(@NonNls @NotNull String path) { |
|
Class callerClass = ReflectionUtil.getGrandCallerClass(); |
|
if (callerClass == null) return null; |
|
return findIcon(path, callerClass); |
|
} |
|
|
|
@NotNull |
|
public static Icon getIcon(@NotNull String path, @NotNull final Class aClass) { |
|
final Icon icon = findIcon(path, aClass); |
|
if (icon == null) { |
|
System.err.println("Icon cannot be found in '" + path + "', aClass='" + aClass + "'"); |
|
} |
|
return icon; |
|
} |
|
|
|
public static void activate() { |
|
ourIsActivated.set(true); |
|
} |
|
|
|
public static void disable() { |
|
ourIsActivated.set(false); |
|
} |
|
|
|
public static boolean isLoaderDisabled() { |
|
return !ourIsActivated.get(); |
|
} |
|
|
|
/** |
|
* This method is for test purposes only |
|
*/ |
|
static void enableSaveRealIconPath() { |
|
ourIsSaveRealIconPath.set(true); |
|
} |
|
|
|
/** |
|
* Might return null if icon was not found. |
|
* Use only if you expected null return value, otherwise see {@link IconLoader#getIcon(String, Class)} |
|
*/ |
|
@Nullable |
|
public static Icon findIcon(@NotNull final String path, @NotNull final Class aClass) { |
|
return findIcon(path, aClass, false); |
|
} |
|
|
|
@Nullable |
|
public static Icon findIcon(@NotNull String path, @NotNull final Class aClass, boolean computeNow) { |
|
return findIcon(path, aClass, computeNow, STRICT); |
|
} |
|
|
|
@Nullable |
|
public static Icon findIcon(@NotNull String path, @NotNull final Class aClass, boolean computeNow, boolean strict) { |
|
String originalPath = path; |
|
path = patchPath(path); |
|
if (isReflectivePath(path)) return getReflectiveIcon(path, aClass.getClassLoader()); |
|
|
|
URL myURL = aClass.getResource(path); |
|
if (myURL == null) { |
|
if (strict) throw new RuntimeException("Can't find icon in '" + path + "' near " + aClass); |
|
return null; |
|
} |
|
final Icon icon = findIcon(myURL); |
|
if (icon instanceof CachedImageIcon) { |
|
((CachedImageIcon)icon).myOriginalPath = originalPath; |
|
((CachedImageIcon)icon).myClassLoader = aClass.getClassLoader(); |
|
} |
|
return icon; |
|
} |
|
|
|
private static String patchPath(@NotNull String path) { |
|
// for (IconPathPatcher patcher : ourPatchers) { |
|
// String newPath = patcher.patchPath(path); |
|
// if (newPath != null) { |
|
// path = newPath; |
|
// } |
|
// } |
|
return path; |
|
} |
|
|
|
private static boolean isReflectivePath(@NotNull String path) { |
|
List<String> paths = StringUtil.split(path, "."); |
|
return paths.size() > 1 && paths.get(0).endsWith("Icons"); |
|
} |
|
|
|
@Nullable |
|
public static Icon findIcon(URL url) { |
|
return findIcon(url, true); |
|
} |
|
|
|
@Nullable |
|
public static Icon findIcon(URL url, boolean useCache) { |
|
if (url == null) { |
|
return null; |
|
} |
|
CachedImageIcon icon = ourIconsCache.get(url); |
|
if (icon == null) { |
|
icon = new CachedImageIcon(url); |
|
if (useCache) { |
|
icon = ConcurrencyUtil.cacheOrGet(ourIconsCache, url, icon); |
|
} |
|
} |
|
return icon; |
|
} |
|
|
|
@Nullable |
|
public static Icon findIcon(@NotNull String path, @NotNull ClassLoader classLoader) { |
|
String originalPath = path; |
|
path = patchPath(path); |
|
if (isReflectivePath(path)) return getReflectiveIcon(path, classLoader); |
|
if (!StringUtil.startsWithChar(path, '/')) return null; |
|
|
|
final URL url = classLoader.getResource(path.substring(1)); |
|
final Icon icon = findIcon(url); |
|
if (icon instanceof CachedImageIcon) { |
|
((CachedImageIcon)icon).myOriginalPath = originalPath; |
|
((CachedImageIcon)icon).myClassLoader = classLoader; |
|
} |
|
return icon; |
|
} |
|
|
|
@Nullable |
|
private static ImageIcon checkIcon(final Image image, @NotNull URL url) { |
|
if (image == null || image.getHeight(LabelHolder.ourFakeComponent) < 1) { // image wasn't loaded or broken |
|
return null; |
|
} |
|
|
|
final Icon icon = getIcon(image); |
|
if (icon != null && !isGoodSize(icon)) { |
|
return EMPTY_ICON; |
|
} |
|
|
|
return (ImageIcon)icon; |
|
} |
|
|
|
public static boolean isGoodSize(@NotNull final Icon icon) { |
|
return icon.getIconWidth() > 0 && icon.getIconHeight() > 0; |
|
} |
|
|
|
/** |
|
* Gets (creates if necessary) disabled icon based on the passed one. |
|
* |
|
* @return <code>ImageIcon</code> constructed from disabled image of passed icon. |
|
*/ |
|
@Nullable |
|
public static Icon getDisabledIcon(Icon icon) { |
|
if (icon instanceof LazyIcon) icon = ((LazyIcon)icon).getOrComputeIcon(); |
|
if (icon == null) return null; |
|
|
|
Icon disabledIcon = ourIcon2DisabledIcon.get(icon); |
|
if (disabledIcon == null) { |
|
if (!isGoodSize(icon)) { |
|
return EMPTY_ICON; |
|
} |
|
final int scale = UIUtil.isRetina() ? 2 : 1; |
|
@SuppressWarnings("UndesirableClassUsage") |
|
BufferedImage image = new BufferedImage(scale*icon.getIconWidth(), scale*icon.getIconHeight(), BufferedImage.TYPE_INT_ARGB); |
|
final Graphics2D graphics = image.createGraphics(); |
|
|
|
graphics.setColor(UIUtil.TRANSPARENT_COLOR); |
|
graphics.fillRect(0, 0, icon.getIconWidth(), icon.getIconHeight()); |
|
graphics.scale(scale, scale); |
|
icon.paintIcon(LabelHolder.ourFakeComponent, graphics, 0, 0); |
|
|
|
graphics.dispose(); |
|
|
|
Image img = ImageUtil.filter(image, UIUtil.getGrayFilter()); |
|
if (UIUtil.isRetina()) img = RetinaImage.createFrom(img); |
|
|
|
disabledIcon = new JBImageIcon(img); |
|
ourIcon2DisabledIcon.put(icon, disabledIcon); |
|
} |
|
return disabledIcon; |
|
} |
|
|
|
public static Icon getTransparentIcon(@NotNull final Icon icon) { |
|
return getTransparentIcon(icon, 0.5f); |
|
} |
|
|
|
public static Icon getTransparentIcon(@NotNull final Icon icon, final float alpha) { |
|
return new RetrievableIcon() { |
|
@Nullable |
|
@Override |
|
public Icon retrieveIcon() { |
|
return icon; |
|
} |
|
|
|
@Override |
|
public int getIconHeight() { |
|
return icon.getIconHeight(); |
|
} |
|
|
|
@Override |
|
public int getIconWidth() { |
|
return icon.getIconWidth(); |
|
} |
|
|
|
@Override |
|
public void paintIcon(final Component c, final Graphics g, final int x, final int y) { |
|
final Graphics2D g2 = (Graphics2D)g; |
|
final Composite saveComposite = g2.getComposite(); |
|
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_ATOP, alpha)); |
|
icon.paintIcon(c, g2, x, y); |
|
g2.setComposite(saveComposite); |
|
} |
|
}; |
|
} |
|
|
|
/** |
|
* Gets a snapshot of the icon, immune to changes made by these calls: |
|
* {@link IconLoader#setScale(float)}, {@link IconLoader#setFilter(ImageFilter)}, {@link IconLoader#setUseDarkIcons(boolean)} |
|
* |
|
* @param icon the source icon |
|
* @return the icon snapshot |
|
*/ |
|
@NotNull |
|
public static Icon getIconSnapshot(@NotNull Icon icon) { |
|
if (icon instanceof CachedImageIcon) { |
|
return ((CachedImageIcon)icon).getRealIcon(); |
|
} |
|
return icon; |
|
} |
|
|
|
public static final class CachedImageIcon implements ScalableIcon { |
|
private volatile Object myRealIcon; |
|
public String myOriginalPath; |
|
private ClassLoader myClassLoader; |
|
@NotNull |
|
private URL myUrl; |
|
private volatile boolean dark; |
|
private volatile float scale; |
|
private volatile int numberOfPatchers = 0; |
|
|
|
private volatile ImageFilter filter; |
|
private final MyScaledIconsCache myScaledIconsCache = new MyScaledIconsCache(); |
|
|
|
public CachedImageIcon(@NotNull URL url) { |
|
myUrl = url; |
|
dark = USE_DARK_ICONS; |
|
scale = SCALE; |
|
filter = IMAGE_FILTER; |
|
} |
|
|
|
@NotNull |
|
private synchronized ImageIcon getRealIcon() { |
|
if (isLoaderDisabled() && (myRealIcon == null || dark != USE_DARK_ICONS || scale != SCALE || filter != IMAGE_FILTER)) return EMPTY_ICON; |
|
|
|
if (!isValid()) { |
|
myRealIcon = null; |
|
dark = USE_DARK_ICONS; |
|
scale = SCALE; |
|
filter = IMAGE_FILTER; |
|
myScaledIconsCache.clear(); |
|
} |
|
Object realIcon = myRealIcon; |
|
if (realIcon instanceof Icon) return (ImageIcon)realIcon; |
|
|
|
ImageIcon icon; |
|
if (realIcon instanceof Reference) { |
|
icon = ((Reference<ImageIcon>)realIcon).get(); |
|
if (icon != null) return (ImageIcon)icon; |
|
} |
|
|
|
Image image = ImageLoader.loadFromUrl(myUrl, true, filter); |
|
icon = checkIcon(image, myUrl); |
|
|
|
if (icon != null) { |
|
if (icon.getIconWidth() < 50 && icon.getIconHeight() < 50) { |
|
realIcon = icon; |
|
} |
|
else { |
|
realIcon = new SoftReference<ImageIcon>(icon); |
|
} |
|
myRealIcon = realIcon; |
|
} |
|
|
|
return icon == null ? EMPTY_ICON : icon; |
|
} |
|
|
|
private boolean isValid() { |
|
return dark == USE_DARK_ICONS && scale == SCALE && filter == IMAGE_FILTER; |
|
} |
|
|
|
@Override |
|
public void paintIcon(Component c, Graphics g, int x, int y) { |
|
getRealIcon().paintIcon(c, g, x, y); |
|
} |
|
|
|
@Override |
|
public int getIconWidth() { |
|
return getRealIcon().getIconWidth(); |
|
} |
|
|
|
@Override |
|
public int getIconHeight() { |
|
return getRealIcon().getIconHeight(); |
|
} |
|
|
|
@Override |
|
public String toString() { |
|
return myUrl.toString(); |
|
} |
|
|
|
@Override |
|
public Icon scale(float scaleFactor) { |
|
if (scaleFactor == 1f) { |
|
return this; |
|
} |
|
|
|
if (!isValid()) getRealIcon(); // force state update & cache reset |
|
|
|
Icon icon = myScaledIconsCache.getScaledIcon(scaleFactor); |
|
if (icon != null) { |
|
return icon; |
|
} |
|
return this; |
|
} |
|
|
|
private class MyScaledIconsCache { |
|
// Map {false -> image}, {true -> image@2x} |
|
private Map<Boolean, SoftReference<Image>> origImagesCache = Collections.synchronizedMap(new HashMap<Boolean, SoftReference<Image>>(2)); |
|
|
|
private static final int SCALED_ICONS_CACHE_LIMIT = 5; |
|
|
|
// Map {effective scale -> icon} |
|
private Map<Float, SoftReference<Icon>> scaledIconsCache = Collections.synchronizedMap(new LinkedHashMap<Float, SoftReference<Icon>>(SCALED_ICONS_CACHE_LIMIT) { |
|
@Override |
|
public boolean removeEldestEntry(Map.Entry<Float, SoftReference<Icon>> entry) { |
|
return size() > SCALED_ICONS_CACHE_LIMIT; |
|
} |
|
}); |
|
|
|
public Image getOrigImage(boolean retina) { |
|
Image img = SoftReference.dereference(origImagesCache.get(retina)); |
|
|
|
if (img == null) { |
|
img = ImageLoader.loadFromUrl(myUrl, UIUtil.isUnderDarcula(), retina, filter); |
|
origImagesCache.put(retina, new SoftReference<Image>(img)); |
|
} |
|
return img; |
|
} |
|
|
|
public Icon getScaledIcon(float scale) { |
|
float effectiveScale = scale * JBUI.scale(1f); |
|
Icon icon = SoftReference.dereference(scaledIconsCache.get(effectiveScale)); |
|
|
|
if (icon == null) { |
|
boolean needRetinaImage = (effectiveScale >= 1.5f || UIUtil.isRetina()); |
|
Image image = getOrigImage(needRetinaImage); |
|
|
|
if (image != null) { |
|
Image iconImage = getRealIcon().getImage(); |
|
int width = (int)(ImageUtil.getRealWidth(iconImage) * scale); |
|
int height = (int)(ImageUtil.getRealHeight(iconImage) * scale); |
|
|
|
Image resizedImage = Scalr.resize(ImageUtil.toBufferedImage(image), Scalr.Method.ULTRA_QUALITY, width, height); |
|
if (UIUtil.isRetina()) resizedImage = RetinaImage.createFrom(resizedImage); |
|
|
|
icon = getIcon(resizedImage); |
|
scaledIconsCache.put(effectiveScale, new SoftReference<Icon>(icon)); |
|
} |
|
} |
|
return icon; |
|
} |
|
|
|
public void clear() { |
|
scaledIconsCache.clear(); |
|
origImagesCache.clear(); |
|
} |
|
} |
|
} |
|
|
|
public abstract static class LazyIcon implements Icon { |
|
private boolean myWasComputed; |
|
private Icon myIcon; |
|
private boolean isDarkVariant = USE_DARK_ICONS; |
|
private float scale = SCALE; |
|
// private int numberOfPatchers = 0; |
|
private ImageFilter filter = IMAGE_FILTER; |
|
|
|
@Override |
|
public void paintIcon(Component c, Graphics g, int x, int y) { |
|
final Icon icon = getOrComputeIcon(); |
|
if (icon != null) { |
|
icon.paintIcon(c, g, x, y); |
|
} |
|
} |
|
|
|
@Override |
|
public int getIconWidth() { |
|
final Icon icon = getOrComputeIcon(); |
|
return icon != null ? icon.getIconWidth() : 0; |
|
} |
|
|
|
@Override |
|
public int getIconHeight() { |
|
final Icon icon = getOrComputeIcon(); |
|
return icon != null ? icon.getIconHeight() : 0; |
|
} |
|
|
|
protected final synchronized Icon getOrComputeIcon() { |
|
if (!myWasComputed || isDarkVariant != USE_DARK_ICONS || scale != SCALE || filter != IMAGE_FILTER /*|| numberOfPatchers != ourPatchers.size()*/) { |
|
isDarkVariant = USE_DARK_ICONS; |
|
scale = SCALE; |
|
filter = IMAGE_FILTER; |
|
myWasComputed = true; |
|
// numberOfPatchers = ourPatchers.size(); |
|
myIcon = compute(); |
|
} |
|
|
|
return myIcon; |
|
} |
|
|
|
public final void load() { |
|
getIconWidth(); |
|
} |
|
|
|
protected abstract Icon compute(); |
|
} |
|
|
|
private static class LabelHolder { |
|
/** |
|
* To get disabled icon with paint it into the image. Some icons require |
|
* not null component to paint. |
|
*/ |
|
private static final JComponent ourFakeComponent = new JLabel(); |
|
} |
|
}
|
|
|