Repository navigation
Allow rewriting image URLs via an IImageURLModifier strategy #4399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||||||
| * | ||||||
| * @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); | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
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 { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||||||
|
|
||||||
| /** 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; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 / |
||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * @return the installed modifier, or <code>null</code> if none is installed | ||||||
| */ | ||||||
| public static IImageURLModifier getURLModifier() { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||
| return urlModifier; | ||||||
| } | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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; | ||||||||||||||||||
|
|
@@ -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; | ||||||||||||||||||
| }; | ||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
@@ -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); | ||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
|
||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar here
Suggested change
|
||||||||||||||||||
| } | ||||||||||||||||||
| 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 | ||||||||||||||||||
|
|
||||||||||||||||||
| 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(); | ||
| } | ||
| } |
There was a problem hiding this comment.
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.