IDEA-146564 support SVG icons

This commit is contained in:
Anton Tarasov
2015-10-21 13:07:00 +03:00
parent 3b3dfa0394
commit 9f365362c4
13 changed files with 377 additions and 71 deletions
+2
View File
@@ -968,6 +968,8 @@ def layout_core(String home, String target) {
include(name: "snappy-in-java-0.3.1.jar")
include(name: "jayatana-1.2.4.jar")
include(name: "imgscalr-lib-4.2.jar")
include(name: "batik-all-1.8.jar")
include(name: "xmlgraphics-commons-1.5.jar")
}
}
}
+2
View File
@@ -211,6 +211,8 @@ libraryLicense(name: "Guava", version: "17.0", license: "Apache 2.0", url: "http
libraryLicense(name: "hamcrest", version: "1.3", license: "BSD", url: "http://hamcrest.org/", licenseUrl: "http://opensource.org/licenses/BSD-3-Clause")
libraryLicense(name: "HttpComponents HttpClient", libraryName: "http-client", version: "4.3.2", license: "Apache 2.0", url: "http://hc.apache.org/httpcomponents-client-ga/index.html")
libraryLicense(name: "imgscalr", libraryName: "imgscalr", version: "4.2", license: "Apache 2.0", licenseUrl: "http://www.apache.org/licenses/LICENSE-2.0.txt", url: "https://github.com/thebuzzmedia/imgscalr")
libraryLicense(name: "batik", libraryName: "batik", version: "1.8", license: "Apache 2.0", licenseUrl: "http://www.apache.org/licenses/LICENSE-2.0.txt", url: "https://svn.apache.org/repos/asf/xmlgraphics/batik")
libraryLicense(name: "xmlgraphics-commons", libraryName: "xmlgraphics-commons", version: "1.5", license: "Apache 2.0", licenseUrl: "http://www.apache.org/licenses/LICENSE-2.0.txt", url: "https://svn.apache.org/repos/asf/xmlgraphics/commons")
libraryLicense(name: "ini4j", libraryName: "ini4j-0.5.2-patched", version: "0.5.2 (with a patch by JetBrains)", license: "Apache 2.0", url: "http://ini4j.sourceforge.net/", attachedTo: "git4idea")
libraryLicense(name: "ISO RELAX", libraryName: "isorelax.jar", license: "MIT License", url: "http://sourceforge.net/projects/iso-relax/", licenseUrl: "http://www.opensource.org/licenses/mit-license.html")
libraryLicense(name: "Jakarta ORO", libraryName: "OroMatcher", version: "2.0.8", license: "Apache", url: "http://jakarta.apache.org/oro/", licenseUrl: "http://svn.apache.org/repos/asf/jakarta/oro/trunk/LICENSE")
Binary file not shown.
+2
View File
@@ -22,6 +22,8 @@ gson-2.3.1.jar
guava-17.0.jar
hamcrest-core-1.3.jar
imgscalr-lib-4.2.jar
batik-all-1.8.jar
xmlgraphics-commons-1.5.jar
jaxen-1.1.3.jar
jayatana-1.2.4.jar
jcip-annotations.jar
Binary file not shown.
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px" height="16px"
viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
<g>
<path fill-rule="evenodd" clip-rule="evenodd" fill="#717171" d="M20,19c0-0.6-0.1-1.2-0.4-1.7L28,1h-2l-9.4,14.1
C16.4,15,16.2,15,16,15c-0.2,0-0.4,0-0.6,0.1L6,1H4l8.4,16.2C12.1,17.8,12,18.4,12,19c0,0.3,0,0.5,0.1,0.8L10,21l2,6l3.1-4.1
C15.4,23,15.7,23,16,23c0.3,0,0.6,0,0.9-0.1L20,27l2-6l-2.1-1.3C19.9,19.5,20,19.2,20,19z M16,21c-1.1,0-2-0.9-2-2c0-1.1,0.9-2,2-2
c1.1,0,2,0.9,2,2C18,20.1,17.1,21,16,21z"/>
<path fill="#A784BB" d="M8,19c-3.3,0-6,2.7-6,6c0,3.3,2.7,6,6,6c3.3,0,6-2.7,6-6C14,21.7,11.3,19,8,19z M8,27c-1.1,0-2-0.9-2-2
c0-1.1,0.9-2,2-2c1.1,0,2,0.9,2,2C10,26.1,9.1,27,8,27z"/>
<path fill="#A784BB" d="M24,19c-3.3,0-6,2.7-6,6c0,3.3,2.7,6,6,6s6-2.7,6-6C30,21.7,27.3,19,24,19z M24,27c-1.1,0-2-0.9-2-2
c0-1.1,0.9-2,2-2s2,0.9,2,2C26,26.1,25.1,27,24,27z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px" height="16px"
viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
<g>
<path fill="#717171" d="M26.9,31L19,23.2l4.1-4.1l7.8,7.8L26.9,31z"/>
<path fill="#717171" d="M13,1C6.4,1,1,6.4,1,13s5.4,12,12,12c6.6,0,12-5.4,12-12S19.6,1,13,1z M13,21c-4.4,0-8-3.6-8-8s3.6-8,8-8
c4.4,0,8,3.6,8,8S17.4,21,13,21z"/>
<path fill="#4C92BB" d="M19,13c0,3.3-2.7,6-6,6c-3.3,0-6-2.7-6-6s2.7-6,6-6C16.3,7,19,9.7,19,13z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 619 B

+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="16px" height="16px"
viewBox="0 0 32 32" enable-background="new 0 0 32 32" xml:space="preserve">
<g>
<path fill="#4C92BB" d="M9.7,22.2C7.5,20.6,6,18,6,15c0-4.6,3.5-8.4,8-8.9V2C7.3,2.5,2,8.1,2,15c0,4.1,1.9,7.7,4.9,10.1L2,30h14
l-0.1-14L9.7,22.2z"/>
<path fill="#4C92BB" d="M25.1,6.9L30,2H16l0.1,14l6.2-6.2C24.5,11.4,26,14,26,17c0,4.6-3.5,8.4-8,8.9v4c6.7-0.5,12-6.1,12-12.9
C30,12.9,28.1,9.3,25.1,6.9z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 594 B

@@ -85,6 +85,8 @@ ide.appIcon.progress=true
ide.appIcon.badge=true
ide.appIcon.requestAttention=true
ide.svg.icon=false
ide.windowSystem.hScrollChars=5
ide.windowSystem.vScrollChars=5
ide.windowSystem.autoShowProcessPopup=false
@@ -15,8 +15,11 @@
*/
package com.intellij.openapi.util;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.registry.RegistryValue;
import com.intellij.openapi.util.registry.RegistryValueListener;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.reference.SoftReference;
import com.intellij.util.ConcurrencyUtil;
@@ -90,6 +93,15 @@ public final class IconLoader {
ourDeprecatedIconsReplacements.put("/vcs/messageHistory.png", "AllIcons.General.MessageHistory");
}
private static Disposable myDisposable = Disposer.newDisposable();
static {
Registry.get("ide.svg.icon").addListener(new RegistryValueListener.Adapter() {
public void afterValueChanged(RegistryValue value) {
clearCache();
}
}, myDisposable);
}
private static final ImageIcon EMPTY_ICON = new ImageIcon(UIUtil.createImage(1, 1, BufferedImage.TYPE_3BYTE_BGR)) {
@NonNls
public String toString() {
@@ -21,7 +21,7 @@ import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.BufferExposingByteArrayOutputStream;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.io.FileUtilRt;
import com.intellij.util.io.URLUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.ui.ImageUtil;
import com.intellij.util.ui.JBUI;
import com.intellij.util.ui.UIUtil;
@@ -38,11 +38,164 @@ import java.io.InputStream;
import java.io.Serializable;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@Deprecated
public class ImageLoader implements Serializable {
private static class ImageDesc {
public enum Type {
PNG,
SVG {
@Override
public Image load(URL url, InputStream is, float scale) throws IOException {
return SVGLoader.load(url, is, scale);
}
},
UNDEFINED;
public Image load(URL url, InputStream stream, float scale) throws IOException {
return ImageLoader.load(stream, (int)scale);
}
}
public final String path;
public final @Nullable Class cls; // resource class if present
public final float scale; // initial scale factor
public final Type type;
public ImageDesc(String path, Class cls, float scale, Type type) {
this.path = path;
this.cls = cls;
this.scale = scale;
this.type = type;
}
@Nullable
public Image load() throws IOException {
InputStream stream = null;
URL url = null;
if (cls != null) {
stream = cls.getResourceAsStream(path);
if (stream == null) return null;
}
if (stream == null) {
url = new URL(path);
stream = url.openStream();
}
return type.load(url, stream, scale);
}
}
private static class ImageDescList extends ArrayList<ImageDesc> {
private ImageDescList() {}
@Nullable
public Image load() {
return load(ImageConverterChain.create());
}
@Nullable
public Image load(@NotNull ImageConverterChain converters) {
for (ImageDesc desc : this) {
try {
Image image = desc.load();
if (image == null) continue;
return converters.convert(image, desc);
}
catch (IOException ignore) {
}
}
return null;
}
public static ImageDescList create(@NotNull String file,
@Nullable Class cls,
boolean dark,
boolean retina,
boolean allowFloatScaling)
{
ImageDescList vars = new ImageDescList();
if (retina || dark) {
final String name = FileUtil.getNameWithoutExtension(file);
final String ext = FileUtilRt.getExtension(file);
float scale = calcScaleFactor(allowFloatScaling);
// TODO: allow SVG images to freely scale on Retina
if (Registry.is("ide.svg.icon") && dark) {
vars.add(new ImageDesc(name + "_dark.svg", cls, UIUtil.isRetina() ? 2f : scale, ImageDesc.Type.SVG));
}
if (Registry.is("ide.svg.icon")) {
vars.add(new ImageDesc(name + ".svg", cls, UIUtil.isRetina() ? 2f : scale, ImageDesc.Type.SVG));
}
if (dark && retina) {
vars.add(new ImageDesc(name + "@2x_dark." + ext, cls, 2f, ImageDesc.Type.PNG));
}
if (dark) {
vars.add(new ImageDesc(name + "_dark." + ext, cls, 1f, ImageDesc.Type.PNG));
}
if (retina) {
vars.add(new ImageDesc(name + "@2x." + ext, cls, 2f, ImageDesc.Type.PNG));
}
}
vars.add(new ImageDesc(file, cls, 1f, ImageDesc.Type.PNG));
return vars;
}
}
private interface ImageConverter {
Image convert(@Nullable Image source, ImageDesc desc);
}
private static class ImageConverterChain extends ArrayList<ImageConverter> {
private ImageConverterChain() {}
public static ImageConverterChain create() {
return new ImageConverterChain();
}
public ImageConverterChain withFilter(final ImageFilter filter) {
return with(new ImageConverter() {
@Override
public Image convert(Image source, ImageDesc desc) {
return ImageUtil.filter(source, filter);
}
});
}
public ImageConverterChain withRetina() {
return with(new ImageConverter() {
@Override
public Image convert(Image source, ImageDesc desc) {
if (source != null && UIUtil.isRetina() && desc.scale > 1) {
return RetinaImage.createFrom(source, (int)desc.scale, ourComponent);
}
return source;
}
});
}
public ImageConverterChain with(ImageConverter f) {
add(f);
return this;
}
public Image convert(Image image, ImageDesc desc) {
for (ImageConverter f : this) {
image = f.convert(image, desc);
}
return image;
}
}
public static final Component ourComponent = new Component() {
};
@@ -74,8 +227,7 @@ public class ImageLoader implements Serializable {
@Nullable
public static Image loadFromUrl(@NotNull URL url, boolean allowFloatScaling, ImageFilter filter) {
final float scaleFactor = allowFloatScaling ? JBUI.scale(1f) : JBUI.scale(1f) > 1.5f ? 2f : 1f;
assert scaleFactor >= 1.0f : "By design, only scale factors >= 1.0 are supported";
final float scaleFactor = calcScaleFactor(allowFloatScaling);
// We can't check all 3rd party plugins and convince the authors to add @2x icons.
// (scaleFactor > 1.0) != isRetina() => we should scale images manually.
@@ -86,22 +238,27 @@ public class ImageLoader implements Serializable {
// retina images provides a better result than upscaling non-retina images.
final boolean loadRetinaImages = UIUtil.isRetina() || scaleImages;
for (Pair<String, Integer> each : getFileNames(url.toString(), UIUtil.isUnderDarcula(), loadRetinaImages)) {
try {
Image image = loadFromStream(URLUtil.openStream(new URL(each.first)), each.second, filter);
if (image != null && scaleImages) {
if (each.first.contains("@2x"))
image = scaleImage(image, scaleFactor / 2.0f); // divide by 2.0 as Retina images are 2x the resolution.
else
image = scaleImage(image, scaleFactor);
}
return image;
}
catch (IOException ignore) {
// Image file may not exist, try next one
}
}
return null;
return ImageDescList.create(url.toString(), null, UIUtil.isUnderDarcula(), loadRetinaImages, allowFloatScaling).load(
ImageConverterChain.create().
withFilter(filter).
withRetina().
with(new ImageConverter() {
public Image convert(Image source, ImageDesc desc) {
if (source != null && scaleImages && desc.type != ImageDesc.Type.SVG) {
if (desc.path.contains("@2x"))
return scaleImage(source, scaleFactor / 2.0f); // divide by 2.0 as Retina images are 2x the resolution.
else
return scaleImage(source, scaleFactor);
}
return source;
}
}));
}
private static float calcScaleFactor(boolean allowFloatScaling) {
float scaleFactor = allowFloatScaling ? JBUI.scale(1f) : JBUI.scale(1f) > 1.5f ? 2f : 1f;
assert scaleFactor >= 1.0f : "By design, only scale factors >= 1.0 are supported";
return scaleFactor;
}
@NotNull
@@ -121,14 +278,8 @@ public class ImageLoader implements Serializable {
@Nullable
public static Image loadFromUrl(URL url, boolean dark, boolean retina, ImageFilter filter) {
for (Pair<String, Integer> each : getFileNames(url.toString(), dark, retina || JBUI.isHiDPI())) {
try {
return loadFromStream(URLUtil.openStream(new URL(each.first)), each.second, filter);
}
catch (IOException ignore) {
}
}
return null;
return ImageDescList.create(url.toString(), null, dark, retina, true).
load(ImageConverterChain.create().withFilter(filter).withRetina());
}
@Nullable
@@ -140,43 +291,8 @@ public class ImageLoader implements Serializable {
@Nullable
public static Image loadFromResource(@NonNls @NotNull String path, @NotNull Class aClass) {
for (Pair<String, Integer> each : getFileNames(path)) {
InputStream stream = aClass.getResourceAsStream(each.first);
if (stream == null) continue;
Image image = loadFromStream(stream, each.second);
if (image != null) return image;
}
return null;
}
public static List<Pair<String, Integer>> getFileNames(@NotNull String file) {
return getFileNames(file, UIUtil.isUnderDarcula(), UIUtil.isRetina() || JBUI.scale(1.0f) >= 1.5f);
}
public static List<Pair<String, Integer>> getFileNames(@NotNull String file, boolean dark, boolean retina) {
if (retina || dark) {
List<Pair<String, Integer>> answer = new ArrayList<Pair<String, Integer>>(4);
final String name = FileUtil.getNameWithoutExtension(file);
final String ext = FileUtilRt.getExtension(file);
if (dark && retina) {
answer.add(Pair.create(name + "@2x_dark." + ext, 2));
}
if (dark) {
answer.add(Pair.create(name + "_dark." + ext, 1));
}
if (retina) {
answer.add(Pair.create(name + "@2x." + ext, 2));
}
answer.add(Pair.create(file, 1));
return answer;
}
return Collections.singletonList(Pair.create(file, 1));
return ImageDescList.create(path, aClass, UIUtil.isUnderDarcula(), UIUtil.isRetina() || JBUI.scale(1.0f) >= 1.5f, true).
load(ImageConverterChain.create().withRetina());
}
public static Image loadFromStream(@NotNull final InputStream inputStream) {
@@ -188,7 +304,13 @@ public class ImageLoader implements Serializable {
}
public static Image loadFromStream(@NotNull final InputStream inputStream, final int scale, ImageFilter filter) {
if (scale <= 0) throw new IllegalArgumentException("Scale must 1 or more");
Image image = load(inputStream, scale);
ImageDesc desc = new ImageDesc("", null, scale, ImageDesc.Type.UNDEFINED);
return ImageConverterChain.create().withFilter(filter).withRetina().convert(image, desc);
}
private static Image load(@NotNull final InputStream inputStream, final int scale) {
if (scale <= 0) throw new IllegalArgumentException("Scale must be 1 or greater");
try {
BufferExposingByteArrayOutputStream outputStream = new BufferExposingByteArrayOutputStream();
try {
@@ -207,11 +329,6 @@ public class ImageLoader implements Serializable {
waitForImage(image);
image = ImageUtil.filter(image, filter);
if (UIUtil.isRetina() && scale > 1) {
image = RetinaImage.createFrom(image, scale, ourComponent);
}
return image;
}
catch (Exception ex) {
@@ -224,4 +341,19 @@ public class ImageLoader implements Serializable {
public static boolean isGoodSize(final Icon icon) {
return IconLoader.isGoodSize(icon);
}
/**
* @deprecated use {@link ImageDescList}
*/
public static List<Pair<String, Integer>> getFileNames(@NotNull String file) {
return getFileNames(file, false, false);
}
/**
* @deprecated use {@link ImageDescList}
*/
public static List<Pair<String, Integer>> getFileNames(@NotNull String file, boolean dark, boolean retina) {
new UnsupportedOperationException("unsupported method").printStackTrace();
return new ArrayList<Pair<String, Integer>>();
}
}
@@ -0,0 +1,118 @@
/*
* Copyright 2000-2015 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.intellij.util;
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.ImageTranscoder;
import org.apache.batik.util.XMLResourceDescriptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.w3c.dom.Document;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
/**
* @author tav
*/
public class SVGLoader {
private TranscoderInput input;
private BufferedImage img;
private float width;
private float height;
private enum SizeAttr {
width,
height;
static final int FALLBACK_VALUE = 16;
public int value(@NotNull Document document) {
String value = document.getDocumentElement().getAttribute(name());
assert value.endsWith("px") : "unexpected '" + name() + "' format in " + document.getBaseURI();
try {
return Integer.parseInt(value.substring(0, value.length() - 2));
}
catch (NumberFormatException ex) {
ex.printStackTrace();
return FALLBACK_VALUE;
}
}
}
private class MyTranscoder extends ImageTranscoder {
@Override
public BufferedImage createImage(int w, int h) {
return new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
}
@Override
public void writeImage(BufferedImage img, TranscoderOutput output) throws TranscoderException {
SVGLoader.this.img = img;
}
}
public static Image load(@NotNull URL url, float scale) throws IOException {
return load(url, url.openStream(), scale);
}
public static Image load(@NotNull InputStream stream , float scale) throws IOException {
return load(null, stream, scale);
}
public static Image load(@Nullable URL url, @NotNull InputStream stream , float scale) throws IOException {
try {
return new SVGLoader(url, stream, scale).createImage();
}
catch (TranscoderException ex) {
throw new IOException(ex);
}
}
private SVGLoader(@Nullable URL url, InputStream stream, float scale) throws IOException {
Document document = null;
String uri = null;
try {
uri = url != null ? url.toURI().toString() : null;
}
catch (URISyntaxException ignore) {
}
document = new SAXSVGDocumentFactory(XMLResourceDescriptor.getXMLParserClassName()).
createDocument(uri, stream);
if (document == null) {
throw new IOException("document not created");
}
input = new TranscoderInput(document);
width = SizeAttr.width.value(document) * scale;
height = SizeAttr.height.value(document) * scale;
}
private BufferedImage createImage() throws TranscoderException {
MyTranscoder r = new MyTranscoder();
r.addTranscodingHint(ImageTranscoder.KEY_WIDTH, new Float(width));
r.addTranscodingHint(ImageTranscoder.KEY_HEIGHT, new Float(height));
r.transcode(input, null);
return img;
}
}
+2
View File
@@ -21,6 +21,8 @@
<orderEntry type="library" exported="" name="ForkJoin" level="project" />
<orderEntry type="library" exported="" name="Snappy-Java" level="project" />
<orderEntry type="library" name="imgscalr" level="project" />
<orderEntry type="library" name="batik" level="project" />
<orderEntry type="library" name="xmlgraphics-commons" level="project" />
</component>
<component name="copyright">
<Base>