From 1f298bb1d2b580f079d1e7334ddace74c020a3ef Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Tue, 4 Aug 2026 19:02:06 +0200 Subject: [PATCH] Allow rewriting image URLs via an IImageURLModifier strategy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Icon packs and RCP applications cannot substitute platform icons except through the Equinox transforms hook, which needs a framework extension enabled on the command line. URLImageDescriptor now asks an IImageURLModifier for the URL to load. The workbench installs the highest ranked OSGi service of that type, so an icon pack needs no startup code; RCP applications can call ImageURLModifiers.setURLModifier instead. HiDPI variants are derived from the rewritten URL. Images are not reloaded, so a new icon pack takes effect after a restart. The types live in the x-friends package org.eclipse.jface.internal.provisional.resource until the API settles. Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../META-INF/MANIFEST.MF | 2 +- .../workbench/swt/WorkbenchSWTActivator.java | 17 +++ .../org.eclipse.jface/META-INF/MANIFEST.MF | 1 + .../resource/IImageURLModifier.java | 36 ++++++ .../resource/ImageURLModifiers.java | 44 +++++++ .../jface/resource/URLImageDescriptor.java | 35 ++++-- .../org/eclipse/e4/ui/tests/UIAllTests.java | 2 + .../ImageURLModifierTrackerTest.java | 119 ++++++++++++++++++ .../tests/images/UrlImageDescriptorTest.java | 115 ++++++++++++++++- 9 files changed, 361 insertions(+), 10 deletions(-) create mode 100644 bundles/org.eclipse.jface/src/org/eclipse/jface/internal/provisional/resource/IImageURLModifier.java create mode 100644 bundles/org.eclipse.jface/src/org/eclipse/jface/internal/provisional/resource/ImageURLModifiers.java create mode 100644 tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ImageURLModifierTrackerTest.java diff --git a/bundles/org.eclipse.e4.ui.workbench.swt/META-INF/MANIFEST.MF b/bundles/org.eclipse.e4.ui.workbench.swt/META-INF/MANIFEST.MF index fc7c8b7746f..c21c53df8ad 100644 --- a/bundles/org.eclipse.e4.ui.workbench.swt/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.e4.ui.workbench.swt/META-INF/MANIFEST.MF @@ -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)", diff --git a/bundles/org.eclipse.e4.ui.workbench.swt/src/org/eclipse/e4/ui/internal/workbench/swt/WorkbenchSWTActivator.java b/bundles/org.eclipse.e4.ui.workbench.swt/src/org/eclipse/e4/ui/internal/workbench/swt/WorkbenchSWTActivator.java index 3a7f5a5cad2..7f0b999fb55 100644 --- a/bundles/org.eclipse.e4.ui.workbench.swt/src/org/eclipse/e4/ui/internal/workbench/swt/WorkbenchSWTActivator.java +++ b/bundles/org.eclipse.e4.ui.workbench.swt/src/org/eclipse/e4/ui/internal/workbench/swt/WorkbenchSWTActivator.java @@ -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; @@ -62,6 +64,7 @@ public class WorkbenchSWTActivator implements BundleActivator, DebugOptionsListe private BundleContext context; private ServiceTracker locationTracker; + private ServiceTracker imageURLModifierTracker; private static WorkbenchSWTActivator activator; private DebugTrace trace; @@ -89,11 +92,25 @@ public void start(BundleContext context) throws Exception { Hashtable props = new Hashtable<>(2); props.put(DebugOptions.LISTENER_SYMBOLICNAME, PI_RENDERERS); context.registerService(DebugOptionsListener.class, this, props); + ServiceTracker 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() { diff --git a/bundles/org.eclipse.jface/META-INF/MANIFEST.MF b/bundles/org.eclipse.jface/META-INF/MANIFEST.MF index a08e60a7a2e..22cbff0d077 100644 --- a/bundles/org.eclipse.jface/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.jface/META-INF/MANIFEST.MF @@ -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, diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/internal/provisional/resource/IImageURLModifier.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/internal/provisional/resource/IImageURLModifier.java new file mode 100644 index 00000000000..9fca82f71d7 --- /dev/null +++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/internal/provisional/resource/IImageURLModifier.java @@ -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 - 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 null 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 null to keep the original + */ + URL modifyURL(URL originalURL); +} diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/internal/provisional/resource/ImageURLModifiers.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/internal/provisional/resource/ImageURLModifiers.java new file mode 100644 index 00000000000..e183e2c9fb2 --- /dev/null +++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/internal/provisional/resource/ImageURLModifiers.java @@ -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 - 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 { + + /** 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 null to remove it + */ + public static void setURLModifier(IImageURLModifier modifier) { + urlModifier = modifier; + } + + /** + * @return the installed modifier, or null if none is installed + */ + public static IImageURLModifier getURLModifier() { + return urlModifier; + } +} diff --git a/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLImageDescriptor.java b/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLImageDescriptor.java index 646ca5cb90f..f05bb3c8986 100644 --- a/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLImageDescriptor.java +++ b/bundles/org.eclipse.jface/src/org/eclipse/jface/resource/URLImageDescriptor.java @@ -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,12 +109,12 @@ 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 getZoomedImageSource(URL url, String urlString, int zoom, Function getImage) { + private static R getZoomedImageSource(URL url, int zoom, Function getImage) { URL xUrl = getxURL(url, zoom); if (xUrl != null) { R xdata = getImage.apply(xUrl); @@ -120,9 +122,10 @@ private static R getZoomedImageSource(URL url, String urlString, int zoom, F 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); + IImageURLModifier modifier = ImageURLModifiers.getURLModifier(); + if (result != null && modifier != null) { + URL modified = modifier.modifyURL(result); + 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 diff --git a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/UIAllTests.java b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/UIAllTests.java index f77ed00292a..9a9ef8c92ac 100644 --- a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/UIAllTests.java +++ b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/UIAllTests.java @@ -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; @@ -101,6 +102,7 @@ ExtensionsSortTests.class, HandlerActivationTest.class, ModelAssemblerTests.class, + ImageURLModifierTrackerTest.class, ModelAssemblerFragmentOrderingTests.class, E4ResourceTest.class, AreaRendererTest.class, diff --git a/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ImageURLModifierTrackerTest.java b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ImageURLModifierTrackerTest.java new file mode 100644 index 00000000000..7d4bd87a14e --- /dev/null +++ b/tests/org.eclipse.e4.ui.tests/src/org/eclipse/e4/ui/tests/workbench/ImageURLModifierTrackerTest.java @@ -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 - 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> 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 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 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 a = register("a", 1); + register("b", 5); + + a.setProperties(FrameworkUtil.asDictionary(Map.of(Constants.SERVICE_RANKING, 10))); + + assertEquals("file:/a.png", resolvedURL()); + } + + private ServiceRegistration register(String name, int ranking) { + IImageURLModifier modifier = _ -> { + try { + return URI.create("file:/" + name + ".png").toURL(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + }; + ServiceRegistration 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(); + } +} diff --git a/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/UrlImageDescriptorTest.java b/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/UrlImageDescriptorTest.java index 31ef2696927..804bdb46d26 100644 --- a/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/UrlImageDescriptorTest.java +++ b/tests/org.eclipse.jface.tests/src/org/eclipse/jface/tests/images/UrlImageDescriptorTest.java @@ -29,10 +29,12 @@ import org.eclipse.core.runtime.Adapters; import org.eclipse.core.runtime.IPath; import org.eclipse.jface.internal.InternalPolicy; +import org.eclipse.jface.internal.provisional.resource.ImageURLModifiers; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.ImageData; import org.eclipse.swt.graphics.ImageFileNameProvider; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -202,4 +204,115 @@ public void testAdaptToURL() { assertEquals(imageDataOrig200.height, imageDataURL200.height); } -} \ No newline at end of file + @AfterEach + public void clearURLModifier() { + ImageURLModifiers.setURLModifier(null); + } + + @Test + public void testNoURLModifierInstalledByDefault() { + ImageDescriptor descriptor = ImageDescriptor + .createFromURL(FileImageDescriptorTest.class.getResource("/icons/imagetests/zoomIn.png")); + assertEquals("zoomIn.png", IPath.fromOSString(imagePath(descriptor, 100)).lastSegment()); + } + + @Test + public void testURLModifierRedirectsImageData() { + URL zoomIn = FileImageDescriptorTest.class.getResource("/icons/imagetests/zoomIn.png"); + URL rectangular = FileImageDescriptorTest.class.getResource("/icons/imagetests/rectangular-57x16.png"); + ImageData original = ImageDescriptor.createFromURL(zoomIn).getImageData(100); + ImageData replacement = ImageDescriptor.createFromURL(rectangular).getImageData(100); + assertNotEquals(original.width, replacement.width, "test images are indistinguishable"); + + ImageDescriptor descriptor = ImageDescriptor.createFromURL(zoomIn); + ImageURLModifiers.setURLModifier(url -> zoomIn.toExternalForm().equals(url.toExternalForm()) ? rectangular : url); + + assertEquals(replacement.width, descriptor.getImageData(100).width); + } + + @Test + public void testURLModifierLeavesUnmatchedURLsAlone() { + ImageDescriptor descriptor = ImageDescriptor + .createFromURL(FileImageDescriptorTest.class.getResource("/icons/imagetests/zoomIn.png")); + ImageURLModifiers.setURLModifier(url -> url); + + assertEquals("zoomIn.png", IPath.fromOSString(imagePath(descriptor, 100)).lastSegment()); + } + + @Test + public void testURLModifierReturningNullKeepsOriginalURL() { + ImageDescriptor descriptor = ImageDescriptor + .createFromURL(FileImageDescriptorTest.class.getResource("/icons/imagetests/zoomIn.png")); + ImageURLModifiers.setURLModifier(_ -> null); + + assertEquals("zoomIn.png", IPath.fromOSString(imagePath(descriptor, 100)).lastSegment()); + } + + @Test + public void testURLModifierIsClearedByNull() { + URL zoomIn = FileImageDescriptorTest.class.getResource("/icons/imagetests/zoomIn.png"); + URL rectangular = FileImageDescriptorTest.class.getResource("/icons/imagetests/rectangular-57x16.png"); + ImageDescriptor descriptor = ImageDescriptor.createFromURL(zoomIn); + + ImageURLModifiers.setURLModifier(_ -> rectangular); + assertEquals("rectangular-57x16.png", IPath.fromOSString(imagePath(descriptor, 100)).lastSegment()); + + ImageURLModifiers.setURLModifier(null); + assertEquals("zoomIn.png", IPath.fromOSString(imagePath(descriptor, 100)).lastSegment()); + } + + /** + * The @2x variant is derived from the rewritten URL, so an icon pack only + * supplies the base name. + */ + @Test + public void testURLModifierIsAppliedBeforeHiDpiLookup() { + URL rectangular = FileImageDescriptorTest.class.getResource("/icons/imagetests/rectangular-57x16.png"); + URL zoomIn = FileImageDescriptorTest.class.getResource("/icons/imagetests/zoomIn.png"); + ImageDescriptor descriptor = ImageDescriptor.createFromURL(rectangular); + + ImageURLModifiers.setURLModifier(url -> url.getPath().contains("rectangular-57x16") ? zoomIn : url); + + assertEquals("zoomIn.png", IPath.fromOSString(imagePath(descriptor, 100)).lastSegment()); + assertEquals("zoomIn@2x.png", IPath.fromOSString(imagePath(descriptor, 200)).lastSegment()); + } + + /** + * The scaled name fallback is derived from the rewritten URL too, so an icon + * pack is not mixed with platform icons at higher zoom levels. + */ + @Test + public void testURLModifierIsAppliedBeforeScaledNameLookup() { + URL zoomIn = FileImageDescriptorTest.class.getResource("/icons/imagetests/zoomIn.png"); + URL rectangular = FileImageDescriptorTest.class.getResource("/icons/imagetests/rectangular-57x16.png"); + ImageDescriptor descriptor = ImageDescriptor.createFromURL(zoomIn); + + ImageURLModifiers.setURLModifier(url -> url.getPath().endsWith("/zoomIn.png") ? rectangular : url); + + assertEquals("rectangular-114x32.png", IPath.fromOSString(imagePath(descriptor, 200)).lastSegment()); + assertEquals("rectangular-86x24.png", IPath.fromOSString(imagePath(descriptor, 150)).lastSegment()); + assertEquals(114, descriptor.getImageData(200).width); + } + + @Test + public void testURLModifierIsAppliedToAdaptedURL() { + URL zoomIn = FileImageDescriptorTest.class.getResource("/icons/imagetests/zoomIn.png"); + URL rectangular = FileImageDescriptorTest.class.getResource("/icons/imagetests/rectangular-57x16.png"); + ImageDescriptor descriptor = ImageDescriptor.createFromURL(zoomIn); + + ImageURLModifiers.setURLModifier(_ -> rectangular); + + URL adapted = Adapters.adapt(descriptor, URL.class); + assertNotNull(adapted, "URLImageDescriptor does not adapt to URL"); + assertEquals(rectangular.toExternalForm(), adapted.toExternalForm()); + } + + private static String imagePath(ImageDescriptor descriptor, int zoom) { + ImageFileNameProvider fileNameProvider = Adapters.adapt(descriptor, ImageFileNameProvider.class); + assertNotNull(fileNameProvider, "URLImageDescriptor does not adapt to ImageFileNameProvider"); + String path = fileNameProvider.getImagePath(zoom); + assertNotNull(path, "URLImageDescriptor ImageFileNameProvider does not return the " + zoom + "% path"); + return path; + } + +}