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