Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions com.vogella.ide.iconreplacer/.classpath
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-25"/>
<classpathentry kind="con" path="org.eclipse.pde.core.requiredPlugins"/>
<classpathentry kind="src" path="src"/>
<classpathentry kind="output" path="bin"/>
</classpath>
28 changes: 28 additions & 0 deletions com.vogella.ide.iconreplacer/.project
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>com.vogella.ide.iconreplacer</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.ManifestBuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.pde.SchemaBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.pde.PluginNature</nature>
<nature>org.eclipse.jdt.core.javanature</nature>
</natures>
</projectDescription>
17 changes: 17 additions & 0 deletions com.vogella.ide.iconreplacer/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: Icon Replacer
Bundle-SymbolicName: com.vogella.ide.iconreplacer;singleton:=true
Bundle-Version: 1.0.0.qualifier
Bundle-Vendor: VOGELLA
Require-Bundle: org.eclipse.core.runtime,
org.eclipse.ui,
org.eclipse.equinox.app,
org.eclipse.jface
Bundle-RequiredExecutionEnvironment: JavaSE-25
Import-Package: com.google.gson;version="[2.11.0,3.0.0)",
com.google.gson.reflect;version="[2.11.0,3.0.0)",
jakarta.inject;version="[2.0.0,3.0.0)",
org.osgi.framework;version="[1.10.0,2.0.0)",
org.osgi.framework.wiring;version="[1.2.0,2.0.0)"
Automatic-Module-Name: com.vogella.ide.iconreplacer
6 changes: 6 additions & 0 deletions com.vogella.ide.iconreplacer/build.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
source.. = src/
output.. = bin/
bin.includes = plugin.xml,\
META-INF/,\
.,\
schema/
40 changes: 40 additions & 0 deletions com.vogella.ide.iconreplacer/plugin.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<?eclipse version="3.4"?>
<plugin>

<extension-point
id="iconpack"
name="Icon Pack"
schema="schema/iconpack.exsd"/>

<extension
point="org.eclipse.ui.commands">
<command
id="com.vogella.ide.iconreplacer.applyIconPack"
name="Apply Icon Pack"
description="Patches bundle JARs with the contributed icon pack and restarts Eclipse"/>
</extension>

<extension
point="org.eclipse.ui.handlers">
<handler
commandId="com.vogella.ide.iconreplacer.applyIconPack"
class="com.vogella.ide.iconreplacer.IconReplacerHandler"/>
</extension>

<extension
point="org.eclipse.ui.menus">
<menuContribution
locationURI="menu:org.eclipse.ui.main.menu?after=additions">
<menu
id="com.vogella.ide.iconreplacer.menu"
label="Icons">
<command
commandId="com.vogella.ide.iconreplacer.applyIconPack"
label="Apply Icon Pack"
style="push"/>
</menu>
</menuContribution>
</extension>

</plugin>
103 changes: 103 additions & 0 deletions com.vogella.ide.iconreplacer/schema/iconpack.exsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?xml version='1.0' encoding='UTF-8'?>
<!-- Schema file written by PDE -->
<schema targetNamespace="com.vogella.ide.iconreplacer" xmlns="http://www.w3.org/2001/XMLSchema">
<annotation>
<appinfo>
<meta.schema plugin="com.vogella.ide.iconreplacer" id="iconpack" name="Icon Pack"/>
</appinfo>
<documentation>
Allows plug-ins to contribute an icon pack: a mapping file and a folder of
replacement SVG files. The mapping file uses the format:

&lt;pre&gt;
{
"terminal.svg": [
"org.eclipse.ui.console/icons/full/eview/console_view.svg"
]
}
&lt;/pre&gt;

Each key is a filename in the icon pack folder; each value is an array of
&lt;code&gt;bundleSymbolicName/path&lt;/code&gt; pairs to replace inside that bundle&apos;s JAR.
</documentation>
</annotation>

<element name="extension">
<annotation>
<appinfo>
<meta.element />
</appinfo>
</annotation>
<complexType>
<sequence>
<element ref="iconpack" minOccurs="1" maxOccurs="unbounded"/>
</sequence>
<attribute name="point" type="string" use="required">
<annotation>
<documentation>
a fully qualified identifier of the target extension point
</documentation>
</annotation>
</attribute>
<attribute name="id" type="string">
<annotation>
<documentation>
an optional identifier of the extension instance
</documentation>
</annotation>
</attribute>
<attribute name="name" type="string">
<annotation>
<documentation>
an optional name of the extension instance
</documentation>
</annotation>
</attribute>
</complexType>
</element>

<element name="iconpack">
<complexType>
<attribute name="mappingFile" type="string" use="required">
<annotation>
<documentation>
Bundle-relative path (or absolute URL) to the icon-mapping.json file.
</documentation>
</annotation>
</attribute>
<attribute name="iconFolder" type="string" use="required">
<annotation>
<documentation>
Bundle-relative path to the folder containing the replacement SVG files.
Must end with a &apos;/&apos;.
</documentation>
</annotation>
</attribute>
</complexType>
</element>

<annotation>
<appinfo>
<meta.section type="since"/>
</appinfo>
<documentation>
1.0.0
</documentation>
</annotation>

<annotation>
<appinfo>
<meta.section type="examples"/>
</appinfo>
<documentation>
&lt;pre&gt;
&lt;extension point=&quot;com.vogella.ide.iconreplacer.iconpack&quot;&gt;
&lt;iconpack
mappingFile=&quot;iconpacks/eclipse-dual-tone/icon-mapping.json&quot;
iconFolder=&quot;iconpacks/eclipse-dual-tone/icons/&quot;/&gt;
&lt;/extension&gt;
&lt;/pre&gt;
</documentation>
</annotation>

</schema>
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package com.vogella.ide.iconreplacer;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

import org.eclipse.core.runtime.FileLocator;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.wiring.FrameworkWiring;

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

public class BundlePatcher {

public static void apply(URL mappingUrl, URL iconFolderUrl) throws Exception {
Map<String, List<String>> mapping;
try (InputStreamReader reader = new InputStreamReader(mappingUrl.openStream(), StandardCharsets.UTF_8)) {
Type type = new TypeToken<Map<String, List<String>>>() {}.getType();
mapping = new Gson().fromJson(reader, type);
}

BundleContext ctx = FrameworkUtil.getBundle(BundlePatcher.class).getBundleContext();

// Group all replacements by bundle symbolic name so we rebuild each JAR once
Map<String, Map<String, URL>> replacementsByBundle = new HashMap<>();

for (Map.Entry<String, List<String>> entry : mapping.entrySet()) {
String iconFileName = entry.getKey();
URL replacement = new URL(iconFolderUrl, iconFileName);

for (String targetPath : entry.getValue()) {
int slash = targetPath.indexOf('/');
if (slash < 0) {
continue;
}
String bsn = targetPath.substring(0, slash);
String iconPath = targetPath.substring(slash + 1);

replacementsByBundle
.computeIfAbsent(bsn, k -> new HashMap<>())
.put(iconPath, replacement);
}
}

for (Map.Entry<String, Map<String, URL>> bundleEntry : replacementsByBundle.entrySet()) {
String bsn = bundleEntry.getKey();
Map<String, URL> replacements = bundleEntry.getValue();

Bundle bundle = findBundle(ctx, bsn);
if (bundle == null) {
continue;
}

try (InputStream patched = rebuildJar(bundle, replacements)) {
bundle.update(patched);
}
}

FrameworkWiring wiring = ctx.getBundle(0).adapt(FrameworkWiring.class);
wiring.refreshBundles(null);
}

private static Bundle findBundle(BundleContext ctx, String symbolicName) {
for (Bundle b : ctx.getBundles()) {
if (symbolicName.equals(b.getSymbolicName())) {
return b;
}
}
return null;
}

private static InputStream rebuildJar(Bundle bundle, Map<String, URL> replacements)
throws Exception {
File bundleFile = FileLocator.getBundleFileLocation(bundle)
.orElseThrow(() -> new IOException("Cannot locate bundle file for " + bundle.getSymbolicName()));

ByteArrayOutputStream buffer = new ByteArrayOutputStream();
try (ZipOutputStream zout = new ZipOutputStream(buffer);
ZipInputStream zin = new ZipInputStream(new FileInputStream(bundleFile))) {
ZipEntry entry;
while ((entry = zin.getNextEntry()) != null) {
zout.putNextEntry(new ZipEntry(entry.getName()));
URL replacement = replacements.get(entry.getName());
if (replacement != null) {
try (InputStream in = replacement.openStream()) {
in.transferTo(zout);
}
} else {
zin.transferTo(zout);
}
zout.closeEntry();
}
}
return new ByteArrayInputStream(buffer.toByteArray());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.vogella.ide.iconreplacer;

import java.net.URL;

import org.eclipse.core.runtime.IConfigurationElement;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.handlers.HandlerUtil;
import org.osgi.framework.Bundle;

import org.eclipse.core.commands.AbstractHandler;
import org.eclipse.core.commands.ExecutionEvent;
import org.eclipse.core.commands.ExecutionException;

public class IconReplacerHandler extends AbstractHandler {

private static final String EXTENSION_POINT_ID = "com.vogella.ide.iconreplacer.iconpack";

@Override
public Object execute(ExecutionEvent event) throws ExecutionException {
Shell shell = HandlerUtil.getActiveShell(event);

boolean confirmed = MessageDialog.openConfirm(shell,
"Apply Icon Pack",
"This will patch bundle JARs and restart Eclipse. Continue?");
if (!confirmed) {
return null;
}

try {
IConfigurationElement[] elements =
Platform.getExtensionRegistry().getConfigurationElementsFor(EXTENSION_POINT_ID);

if (elements.length == 0) {
MessageDialog.openInformation(shell, "Apply Icon Pack",
"No icon pack contributions found.");
return null;
}

for (IConfigurationElement element : elements) {
Bundle contributor = Platform.getBundle(element.getContributor().getName());
String mappingFile = element.getAttribute("mappingFile");
String iconFolder = element.getAttribute("iconFolder");

URL mappingUrl = contributor.getEntry(mappingFile);
URL iconFolderUrl = contributor.getEntry(iconFolder);

if (mappingUrl == null) {
throw new IllegalArgumentException(
"Cannot find mappingFile '" + mappingFile + "' in bundle "
+ contributor.getSymbolicName());
}
if (iconFolderUrl == null) {
throw new IllegalArgumentException(
"Cannot find iconFolder '" + iconFolder + "' in bundle "
+ contributor.getSymbolicName());
}

BundlePatcher.apply(mappingUrl, iconFolderUrl);
}

RestartHelper.restartWithClean();

} catch (Exception e) {
MessageDialog.openError(shell, "Icon Replacement Failed", e.getMessage());
}

return null;
}
}
Loading
Loading