From 29e6ece62783fae155eada4820671d867af1d5ec Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Wed, 9 Sep 2026 13:57:47 +0200 Subject: [PATCH] Do not load perspective icons of uninstalled bundles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A perspective persisted in the model outlives the bundle that contributed it, and loading its icon then makes FileLocator log an error per attempt. Check the bundle first and fall back to the default perspective icon. Fixes https://github.com/eclipse-platform/eclipse.platform.ui/issues/4364 Assisted-by: multiple AI agents and layers of automated tooling 🤖 --- .../registry/PerspectiveRegistry.java | 40 +++++++++++++++++-- .../tests/api/IPerspectiveRegistryTest.java | 39 ++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/registry/PerspectiveRegistry.java b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/registry/PerspectiveRegistry.java index 46077dc7e92..edb82787e8c 100644 --- a/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/registry/PerspectiveRegistry.java +++ b/bundles/org.eclipse.ui.workbench/eclipseui/org/eclipse/ui/internal/registry/PerspectiveRegistry.java @@ -33,6 +33,7 @@ import org.eclipse.core.runtime.IExtension; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IExtensionRegistry; +import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.dynamichelpers.IExtensionChangeHandler; import org.eclipse.core.runtime.dynamichelpers.IExtensionTracker; import org.eclipse.e4.core.contexts.ContextInjectionFactory; @@ -58,6 +59,9 @@ */ public class PerspectiveRegistry implements IPerspectiveRegistry, IExtensionChangeHandler { + private static final String PLATFORM_PLUGIN_PREFIX = "platform:/plugin/"; //$NON-NLS-1$ + private static final String PLATFORM_FRAGMENT_PREFIX = "platform:/fragment/"; //$NON-NLS-1$ + @Inject private IExtensionRegistry extensionRegistry; @@ -137,19 +141,49 @@ public void createDescriptor(MPerspective perspective) { String id = perspective.getElementId(); PerspectiveDescriptor newDescriptor = new PerspectiveDescriptor(id, label, originalDescriptor); - if (perspective.getIconURI() != null) { + String iconURI = perspective.getIconURI(); + if (iconURI != null && isIconAvailable(iconURI)) { try { - ImageDescriptor img = ImageDescriptor.createFromURL(new URI(perspective.getIconURI()).toURL()); + ImageDescriptor img = ImageDescriptor.createFromURL(new URI(iconURI).toURL()); newDescriptor.setImageDescriptor(img); } catch (MalformedURLException | URISyntaxException | IllegalArgumentException e) { logger.warn(e, MessageFormat.format("Error on applying configured perspective icon: {0}", //$NON-NLS-1$ - perspective.getIconURI())); + iconURI)); } } descriptors.put(id, newDescriptor); } + /** + * Tells whether the bundle a platform icon URI points to is installed. Loading + * an icon of a missing bundle logs an error per attempt in the platform URL + * layer. + */ + private boolean isIconAvailable(String iconURI) { + String reference = null; + for (String prefix : new String[] { PLATFORM_PLUGIN_PREFIX, PLATFORM_FRAGMENT_PREFIX }) { + if (iconURI.startsWith(prefix)) { + reference = iconURI.substring(prefix.length()).split("/", 2)[0]; //$NON-NLS-1$ + break; + } + } + if (reference == null) { + return true; + } + if (Platform.getBundle(reference) != null) { + return true; + } + // the reference may carry a version, as in "com.example_1.0.0" + int underscore = reference.indexOf('_'); + if (underscore > 0 && Platform.getBundle(reference.substring(0, underscore)) != null) { + return true; + } + logger.warn(MessageFormat.format("Skipping the perspective icon {0} of the not installed bundle {1}", //$NON-NLS-1$ + iconURI, reference)); + return false; + } + /** * Construct a new registry. */ diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/api/IPerspectiveRegistryTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/api/IPerspectiveRegistryTest.java index 6e3c65ed112..b82a72219f2 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/api/IPerspectiveRegistryTest.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/api/IPerspectiveRegistryTest.java @@ -27,8 +27,10 @@ import org.eclipse.e4.ui.workbench.modeling.EModelService; import org.eclipse.ui.IPerspectiveDescriptor; import org.eclipse.ui.IPerspectiveRegistry; +import org.eclipse.ui.ISharedImages; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.internal.WorkbenchImages; import org.eclipse.ui.internal.WorkbenchWindow; import org.eclipse.ui.tests.harness.util.ArrayUtil; import org.junit.Before; @@ -139,6 +141,43 @@ public void testModelPerspective() { } } + /** + * A perspective whose contributing bundle is gone must fall back to the default + * icon instead of loading the one its model element names. + */ + @Test + public void testModelPerspectiveWithUninstalledIconBundle() { + WorkbenchWindow window = (WorkbenchWindow) PlatformUI.getWorkbench().getActiveWorkbenchWindow(); + EModelService modelService = window.getService(EModelService.class); + + List stacks = modelService.findElements(window.getModel(), null, MPerspectiveStack.class); + assertFalse("expected a perspective stack in the active window", stacks.isEmpty()); + MPerspectiveStack stack = stacks.get(0); + + String id = "org.eclipse.ui.tests.perspectiveWithUninstalledIconBundle"; + MPerspective perspective = modelService.createModelElement(MPerspective.class); + perspective.setElementId(id); + perspective.setLabel("Perspective With Uninstalled Icon Bundle"); + perspective.setIconURI("platform:/plugin/org.eclipse.ui.tests.uninstalled/icons/missing.svg"); + perspective.setToBeRendered(false); + stack.getChildren().add(perspective); + + try { + fReg.getPerspectives(); // registers perspectives added to the model + IPerspectiveDescriptor descriptor = fReg.findPerspectiveWithId(id); + assertNotNull("model-contributed perspective must be resolvable by id", descriptor); + assertEquals("the default perspective icon must be used for an uninstalled bundle", + WorkbenchImages.getImageDescriptor(ISharedImages.IMG_ETOOL_DEF_PERSPECTIVE), + descriptor.getImageDescriptor()); + } finally { + stack.getChildren().remove(perspective); + IPerspectiveDescriptor descriptor = fReg.findPerspectiveWithId(id); + if (descriptor != null) { + fReg.deletePerspective(descriptor); + } + } + } + @Test @Ignore public void XXXtestDeleteClonedPerspective() {