Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Bundle-Activator: org.eclipse.e4.ui.internal.workbench.swt.WorkbenchSWTActivator
Require-Bundle: org.eclipse.e4.ui.workbench;bundle-version="0.10.0",
org.eclipse.e4.core.services;bundle-version="1.0.0",
org.eclipse.e4.ui.services;bundle-version="0.1.0",
org.eclipse.jface;bundle-version="[3.39.0,4.0.0)",
org.eclipse.jface;bundle-version="[3.41.0,4.0.0)",
org.eclipse.e4.ui.dialogs;bundle-version="1.1.600",
org.eclipse.core.databinding;bundle-version="[1.2.0,2.0.0)",
org.eclipse.jface.databinding;bundle-version="[1.3.0,2.0.0)",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.DialogSettings;
import org.eclipse.jface.dialogs.IDialogSettings;
import org.eclipse.jface.internal.provisional.resource.IImageURLModifier;
import org.eclipse.jface.internal.provisional.resource.ImageURLModifiers;
import org.eclipse.osgi.service.datalocation.Location;
import org.eclipse.osgi.service.debug.DebugOptions;
import org.eclipse.osgi.service.debug.DebugOptionsListener;
Expand All @@ -62,6 +64,7 @@ public class WorkbenchSWTActivator implements BundleActivator, DebugOptionsListe

private BundleContext context;
private ServiceTracker<?, Location> locationTracker;
private ServiceTracker<IImageURLModifier, IImageURLModifier> imageURLModifierTracker;
private static WorkbenchSWTActivator activator;
private DebugTrace trace;

Expand Down Expand Up @@ -89,11 +92,25 @@ public void start(BundleContext context) throws Exception {
Hashtable<String, String> props = new Hashtable<>(2);
props.put(DebugOptions.LISTENER_SYMBOLICNAME, PI_RENDERERS);
context.registerService(DebugOptionsListener.class, this, props);
ServiceTracker<IImageURLModifier, IImageURLModifier> tracker = new ServiceTracker<>(context,
IImageURLModifier.class, null);
tracker.open();
imageURLModifierTracker = tracker;
// getService() returns the highest ranked service and follows ranking changes
ImageURLModifiers.setURLModifier(url -> {
IImageURLModifier modifier = tracker.getService();
return modifier == null ? null : modifier.modifyURL(url);
});
}

@Override
public void stop(BundleContext context) throws Exception {
saveDialogSettings();
if (imageURLModifierTracker != null) {
ImageURLModifiers.setURLModifier(null);
imageURLModifierTracker.close();
imageURLModifierTracker = null;
}
}

public Bundle getBundle() {
Expand Down
1 change: 1 addition & 0 deletions bundles/org.eclipse.jface/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Export-Package: org.eclipse.jface,
org.eclipse.jface.images,
org.eclipse.jface.internal;x-friends:="org.eclipse.ui.workbench,org.eclipse.e4.ui.workbench.renderers.swt,org.eclipse.jface.tests",
org.eclipse.jface.internal.provisional.action;x-friends:="org.eclipse.ui.workbench,org.eclipse.ui.ide",
org.eclipse.jface.internal.provisional.resource;x-friends:="org.eclipse.e4.ui.workbench.swt,org.eclipse.e4.ui.tests,org.eclipse.jface.tests",
org.eclipse.jface.layout,
org.eclipse.jface.menus,
org.eclipse.jface.operation,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*******************************************************************************
* Copyright (c) 2026 vogella GmbH and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.internal.provisional.resource;

import java.net.URL;

/**
* Rewrites image URLs before they are loaded, so icons can be substituted
* without changing the code that creates the image descriptors. The E4
* workbench installs the highest ranked OSGi service of this type.
*
Comment on lines +19 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think implementation details do not belong herem what someone else outside the bundle do with the service interface is out of scope.

* @see ImageURLModifiers#setURLModifier(IImageURLModifier)
*/
@FunctionalInterface
public interface IImageURLModifier {

/**
* Returns the URL to load instead of the given one, or <code>null</code> to
* keep it. Called for every image load, so it must be fast and thread-safe.
*
* @param originalURL the URL the image would be loaded from
* @return the replacement URL, or <code>null</code> to keep the original
*/
URL modifyURL(URL originalURL);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
URL modifyURL(URL originalURL);
Optional<URL> modifyURL(URL originalURL);

to make the optional nature of the return value clear.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*******************************************************************************
* Copyright (c) 2026 vogella GmbH and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.internal.provisional.resource;

/**
* Holds the {@link IImageURLModifier} consulted when an image is loaded from a
* URL.
*/
public final class ImageURLModifiers {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general I wonder if this whole logic is not better placed at URLImageDecriptor and marking the methods as @Provisional - this would even avoid having a getter at all and it seems to logically belongs there.


/** Read from any thread. */
private static volatile IImageURLModifier urlModifier;

private ImageURLModifiers() {
}

/**
* Installs the modifier consulted when an image is loaded from a URL. Images
* created before are not reloaded.
*
* @param modifier the modifier, or <code>null</code> to remove it
*/
public static void setURLModifier(IImageURLModifier modifier) {
urlModifier = modifier;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we allow this to change anytime during the lifecycle or go a similar way like URL Protocol factories does that is you can only set a value if it is currently unset / null and get an exception otherwise. In the current from it can happen that any plugin is messing around and you get mixed results depending on when it is called.

}

/**
* @return the installed modifier, or <code>null</code> if none is installed
*/
public static IImageURLModifier getURLModifier() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think @ruspl-afed would say we should not add any (provisional) API that returns null for missing and instead use

Suggested change
public static IImageURLModifier getURLModifier() {
public static Optional<IImageURLModifier> getURLModifier() {

return urlModifier;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.internal.InternalPolicy;
import org.eclipse.jface.internal.provisional.resource.IImageURLModifier;
import org.eclipse.jface.internal.provisional.resource.ImageURLModifiers;
import org.eclipse.jface.util.Policy;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
Expand Down Expand Up @@ -63,7 +65,7 @@ private ImageFileNameProvider createURLImageFileNameProvider() {
// The calling image will do that itself!
return getFilePath(tempURL, logIOException);
}
return getZoomedImageSource(tempURL, url, zoom, u -> getFilePath(u, logIOException));
return getZoomedImageSource(tempURL, zoom, u -> getFilePath(u, logIOException));
}
return null;
};
Expand Down Expand Up @@ -107,22 +109,23 @@ public ImageData getImageData(int zoom) {
if (zoom == 100 || canLoadAtZoom(tempURL, zoom)) {
return getImageData(tempURL, 100, zoom);
}
return getZoomedImageSource(tempURL, url, zoom, u -> getImageData(u, zoom, zoom));
return getZoomedImageSource(tempURL, zoom, u -> getImageData(u, zoom, zoom));
}
return null;
}

private static <R> R getZoomedImageSource(URL url, String urlString, int zoom, Function<URL, R> getImage) {
private static <R> R getZoomedImageSource(URL url, int zoom, Function<URL, R> getImage) {
URL xUrl = getxURL(url, zoom);
if (xUrl != null) {
R xdata = getImage.apply(xUrl);
if (xdata != null) {
return xdata;
}
}
String xpath = getxPath(urlString, zoom);
// derived from the already modified URL, so the modifier must not run again
String xpath = getxPath(url.toExternalForm(), zoom);
if (xpath != null) {
URL xPathUrl = getURL(xpath);
URL xPathUrl = toURL(xpath);
if (xPathUrl != null) {
return getImage.apply(xPathUrl);
}
Expand Down Expand Up @@ -352,14 +355,30 @@ public Image createImage(boolean returnMissingImageOnError, Device device) {
}
}

/**
* Resolves the given URL string, applying the URL modifier. Every code path of
* this descriptor resolves its URL here.
*/
private static URL getURL(String urlString) {
URL result = null;
URL result = toURL(urlString);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already heavy nested call site here and the null case fall through effectively becomes cumbersome here. I would suggest to simply do

Suggested change
URL result = toURL(urlString);
URL result;
try {
result = new URL(urlString);
} catch (MalformedURLException e) {
Policy.logException(e);
return null;
}

IImageURLModifier modifier = ImageURLModifiers.getURLModifier();
if (result != null && modifier != null) {
URL modified = modifier.modifyURL(result);
if (modified != null) {
return modified;
}
Comment on lines +367 to +369

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar here

Suggested change
if (modified != null) {
result = modified;
}
if (modified != null) {
return modified;
}

}
return result;
}

@SuppressWarnings("deprecation") // keeps the lenient parsing of new URL(String)
private static URL toURL(String urlString) {
try {
result = new URL(urlString);
return new URL(urlString);
} catch (MalformedURLException e) {
Policy.logException(e);
return null;
}
return result;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.eclipse.e4.ui.tests.workbench.ExtensionsSortTests;
import org.eclipse.e4.ui.tests.workbench.HandlerActivationTest;
import org.eclipse.e4.ui.tests.workbench.HandlerTest;
import org.eclipse.e4.ui.tests.workbench.ImageURLModifierTrackerTest;
import org.eclipse.e4.ui.tests.workbench.InjectionEventTest;
import org.eclipse.e4.ui.tests.workbench.MApplicationCommandAccessTest;
import org.eclipse.e4.ui.tests.workbench.MMenuItemTest;
Expand Down Expand Up @@ -101,6 +102,7 @@
ExtensionsSortTests.class,
HandlerActivationTest.class,
ModelAssemblerTests.class,
ImageURLModifierTrackerTest.class,
ModelAssemblerFragmentOrderingTests.class,
E4ResourceTest.class,
AreaRendererTest.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*******************************************************************************
* Copyright (c) 2026 vogella GmbH and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Lars Vogel <Lars.Vogel@vogella.com> - initial API and implementation
*******************************************************************************/
package org.eclipse.e4.ui.tests.workbench;

import static org.junit.jupiter.api.Assertions.assertEquals;

import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.eclipse.core.runtime.Adapters;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.internal.provisional.resource.IImageURLModifier;
import org.eclipse.jface.resource.ImageDescriptor;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.osgi.framework.Bundle;
import org.osgi.framework.BundleContext;
import org.osgi.framework.Constants;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceRegistration;

/**
* Tests that the E4 workbench installs the highest ranked
* {@link IImageURLModifier} service in JFace.
*/
public class ImageURLModifierTrackerTest {

private final BundleContext context = FrameworkUtil.getBundle(ImageURLModifierTrackerTest.class)
.getBundleContext();
private final List<ServiceRegistration<IImageURLModifier>> registrations = new ArrayList<>();
private ImageDescriptor descriptor;

@BeforeAll
public static void startWorkbenchBundle() throws Exception {
// lazily activated, and earlier tests may not have loaded its classes
Platform.getBundle("org.eclipse.e4.ui.workbench.swt").start(Bundle.START_TRANSIENT);
}

@BeforeEach
public void setUp() throws Exception {
descriptor = ImageDescriptor.createFromURL(URI.create("file:/original.png").toURL());
}

@AfterEach
public void tearDown() {
for (ServiceRegistration<IImageURLModifier> registration : registrations) {
try {
registration.unregister();
} catch (IllegalStateException e) {
// already unregistered by the test
}
}
}

@Test
public void testHighestRankedModifierIsInstalled() {
register("a", 1);
register("b", 5);
register("c", 3);

assertEquals("file:/b.png", resolvedURL());
}

@Test
public void testRemovingInstalledModifierFallsBackToNext() {
register("a", 1);
ServiceRegistration<IImageURLModifier> b = register("b", 5);

b.unregister();
assertEquals("file:/a.png", resolvedURL());

registrations.get(0).unregister();
assertEquals("file:/original.png", resolvedURL());
}

@Test
public void testRankingChangeIsApplied() {
ServiceRegistration<IImageURLModifier> a = register("a", 1);
register("b", 5);

a.setProperties(FrameworkUtil.asDictionary(Map.of(Constants.SERVICE_RANKING, 10)));

assertEquals("file:/a.png", resolvedURL());
}

private ServiceRegistration<IImageURLModifier> register(String name, int ranking) {
IImageURLModifier modifier = _ -> {
try {
return URI.create("file:/" + name + ".png").toURL();
} catch (Exception e) {
throw new IllegalStateException(e);
}
};
ServiceRegistration<IImageURLModifier> registration = context.registerService(IImageURLModifier.class,
modifier, FrameworkUtil.asDictionary(Map.of(Constants.SERVICE_RANKING, ranking)));
registrations.add(registration);
return registration;
}

private String resolvedURL() {
return Adapters.adapt(descriptor, URL.class).toExternalForm();
}
}
Loading
Loading