diff --git a/build/scripts/layouts.gant b/build/scripts/layouts.gant index 714698d6b60c..90f111846aa3 100644 --- a/build/scripts/layouts.gant +++ b/build/scripts/layouts.gant @@ -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") } } } diff --git a/build/scripts/libLicenses.gant b/build/scripts/libLicenses.gant index f8ca984c4bb6..41be0514df64 100644 --- a/build/scripts/libLicenses.gant +++ b/build/scripts/libLicenses.gant @@ -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") diff --git a/lib/batik-all-1.8.jar b/lib/batik-all-1.8.jar new file mode 100644 index 000000000000..1a77176e4441 Binary files /dev/null and b/lib/batik-all-1.8.jar differ diff --git a/lib/required_for_dist.txt b/lib/required_for_dist.txt index 3091940e78b9..4c02ec584993 100644 --- a/lib/required_for_dist.txt +++ b/lib/required_for_dist.txt @@ -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 diff --git a/lib/xmlgraphics-commons-1.5.jar b/lib/xmlgraphics-commons-1.5.jar new file mode 100644 index 000000000000..837d72405aa8 Binary files /dev/null and b/lib/xmlgraphics-commons-1.5.jar differ diff --git a/platform/icons/src/actions/menu-cut.svg b/platform/icons/src/actions/menu-cut.svg new file mode 100644 index 000000000000..3fc8253d87f8 --- /dev/null +++ b/platform/icons/src/actions/menu-cut.svg @@ -0,0 +1,14 @@ + + + + + + + + diff --git a/platform/icons/src/actions/menu-find.svg b/platform/icons/src/actions/menu-find.svg new file mode 100644 index 000000000000..d0f948251c83 --- /dev/null +++ b/platform/icons/src/actions/menu-find.svg @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/platform/icons/src/actions/refresh.svg b/platform/icons/src/actions/refresh.svg new file mode 100644 index 000000000000..cb095d18d6db --- /dev/null +++ b/platform/icons/src/actions/refresh.svg @@ -0,0 +1,10 @@ + + + + + + + diff --git a/platform/util/resources/misc/registry.properties b/platform/util/resources/misc/registry.properties index 08089b3bb063..6222b2cec7e2 100644 --- a/platform/util/resources/misc/registry.properties +++ b/platform/util/resources/misc/registry.properties @@ -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 diff --git a/platform/util/src/com/intellij/openapi/util/IconLoader.java b/platform/util/src/com/intellij/openapi/util/IconLoader.java index 490496a90ff1..c73e450b6f18 100644 --- a/platform/util/src/com/intellij/openapi/util/IconLoader.java +++ b/platform/util/src/com/intellij/openapi/util/IconLoader.java @@ -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() { diff --git a/platform/util/src/com/intellij/util/ImageLoader.java b/platform/util/src/com/intellij/util/ImageLoader.java index 6bac4ef1e2a1..39742663a430 100644 --- a/platform/util/src/com/intellij/util/ImageLoader.java +++ b/platform/util/src/com/intellij/util/ImageLoader.java @@ -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 { + 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 { + 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 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 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 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> getFileNames(@NotNull String file) { - return getFileNames(file, UIUtil.isUnderDarcula(), UIUtil.isRetina() || JBUI.scale(1.0f) >= 1.5f); - } - - public static List> getFileNames(@NotNull String file, boolean dark, boolean retina) { - if (retina || dark) { - List> answer = new ArrayList>(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> getFileNames(@NotNull String file) { + return getFileNames(file, false, false); + } + + /** + * @deprecated use {@link ImageDescList} + */ + public static List> getFileNames(@NotNull String file, boolean dark, boolean retina) { + new UnsupportedOperationException("unsupported method").printStackTrace(); + return new ArrayList>(); + } } diff --git a/platform/util/src/com/intellij/util/SVGLoader.java b/platform/util/src/com/intellij/util/SVGLoader.java new file mode 100644 index 000000000000..7a9cd884810d --- /dev/null +++ b/platform/util/src/com/intellij/util/SVGLoader.java @@ -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; + } +} diff --git a/platform/util/util.iml b/platform/util/util.iml index c79780625d05..1bfb29f1b2b9 100644 --- a/platform/util/util.iml +++ b/platform/util/util.iml @@ -21,6 +21,8 @@ + +