From 7306d515c4924bf00faaa990e77e5094ee81fbb8 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Sun, 5 Apr 2026 20:10:23 +0200 Subject: [PATCH 1/2] Show Open/Close Project for mixed selections in Project Explorer Ctrl+A in an expanded Project Explorer produces a mixed selection: projects, child files/folders, and non-adaptable elements such as working set headers. Three problems combined to hide the Open/Close Project actions in that case: 1. The ResourceMgmtActionProvider enablement expression required ALL selected elements to adapt to IResource or IWorkingSet, so the provider was never activated for mixed selections. Fixed by replacing the expression with to always activate the provider. 2. CloseResourceAction and OpenResourceAction.updateSelection() called selectionIsOfType(PROJECT), which returns false whenever any non-IResource element (e.g. a working set header) is present in the selection, even if every resource element is a valid open/closed project. Fixed by checking getSelectedResources() directly. 3. CloseUnrelatedProjectsAction.resourceChanged() had the same selectionIsOfType guard, preventing the action from reacting to project state changes for mixed selections. Fixed analogously. ResourceMgmtActionProvider now overrides getSelectedResources() on the open/close action instances to filter the selection down to IProject elements only, so that files and folders selected alongside projects (expanded Ctrl+A) do not disable the actions. Fixes https://github.com/eclipse-platform/eclipse.platform.ui/issues/3790 --- .../ui/actions/CloseResourceAction.java | 5 +- .../actions/CloseUnrelatedProjectsAction.java | 3 +- .../ui/actions/OpenResourceAction.java | 5 +- .../plugin.xml | 9 +- .../actions/ResourceMgmtActionProvider.java | 25 +++++- .../ResourceMgmtActionProviderTests.java | 86 +++++++++++++++++++ 6 files changed, 118 insertions(+), 15 deletions(-) diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java index fd6aee00f48..5395229474d 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java @@ -229,12 +229,13 @@ protected boolean updateSelection(IStructuredSelection s) { // don't call super since we want to enable if open project is selected. setText(defaultText); setToolTipText(defaultToolTip); - if (!selectionIsOfType(IResource.PROJECT)) { + List selectedResources = getSelectedResources(); + if (selectedResources.isEmpty() || selectedResources.stream().anyMatch(r -> !(r instanceof IProject))) { return false; } boolean hasOpenProjects = false; - Iterator resources = getSelectedResources().iterator(); + Iterator resources = selectedResources.iterator(); while (resources.hasNext()) { IProject currentResource = (IProject) resources.next(); if (currentResource.isOpen()) { diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java index 54eebcb244b..4a698a2bc33 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java @@ -261,7 +261,8 @@ protected List getSelectedResources() { @Override public void resourceChanged(IResourceChangeEvent event) { // don't bother looking at delta if selection not applicable - if (selectionIsOfType(IResource.PROJECT)) { + List selectedResources = super.getSelectedResources(); + if (!selectedResources.isEmpty() && selectedResources.stream().allMatch(r -> r instanceof IProject)) { IResourceDelta delta = event.getDelta(); if (delta != null) { IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED); diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java index cbe3618076f..b22887b5c24 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java @@ -304,12 +304,13 @@ protected boolean updateSelection(IStructuredSelection s) { // selected. setText(IDEWorkbenchMessages.OpenResourceAction_text); setToolTipText(IDEWorkbenchMessages.OpenResourceAction_toolTip); - if (!selectionIsOfType(IResource.PROJECT)) { + List selectedResources = getSelectedResources(); + if (selectedResources.isEmpty() || selectedResources.stream().anyMatch(r -> !(r instanceof IProject))) { return false; } boolean hasClosedProjects = false; - for (IResource currentResource : getSelectedResources()) { + for (IResource currentResource : selectedResources) { if (!((IProject) currentResource).isOpen()) { if (hasClosedProjects) { setText(IDEWorkbenchMessages.OpenResourceAction_text_plural); diff --git a/bundles/org.eclipse.ui.navigator.resources/plugin.xml b/bundles/org.eclipse.ui.navigator.resources/plugin.xml index 7eec1464661..d6007f86353 100644 --- a/bundles/org.eclipse.ui.navigator.resources/plugin.xml +++ b/bundles/org.eclipse.ui.navigator.resources/plugin.xml @@ -237,13 +237,8 @@ class="org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider" id="org.eclipse.ui.navigator.resources.ResourceMgmtActions"> - - - - - - - + + diff --git a/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java index 28111551293..0a6e6bd6ddf 100644 --- a/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java +++ b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java @@ -22,6 +22,7 @@ import org.eclipse.core.resources.ICommand; import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.WorkspaceJob; @@ -207,11 +208,29 @@ boolean hasBuilder(IProject project) { protected void makeActions() { IShellProvider sp = () -> shell; - openProjectAction = new OpenResourceAction(sp); + openProjectAction = new OpenResourceAction(sp) { + @Override + protected synchronized List getSelectedResources() { + return super.getSelectedResources().stream() + .filter(IProject.class::isInstance).toList(); + } + }; - closeProjectAction = new CloseResourceAction(sp); + closeProjectAction = new CloseResourceAction(sp) { + @Override + protected synchronized List getSelectedResources() { + return super.getSelectedResources().stream() + .filter(IProject.class::isInstance).toList(); + } + }; - closeUnrelatedProjectsAction = new CloseUnrelatedProjectsAction(sp); + closeUnrelatedProjectsAction = new CloseUnrelatedProjectsAction(sp) { + @Override + protected List getSelectedResources() { + return super.getSelectedResources().stream() + .filter(IProject.class::isInstance).toList(); + } + }; refreshAction = new RefreshAction(sp) { @Override diff --git a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/resources/ResourceMgmtActionProviderTests.java b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/resources/ResourceMgmtActionProviderTests.java index 128451a678b..9954e56552d 100644 --- a/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/resources/ResourceMgmtActionProviderTests.java +++ b/tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/resources/ResourceMgmtActionProviderTests.java @@ -118,6 +118,67 @@ public void testFillContextMenu_openProjectNoBuilderSelection() throws CoreExcep } } + /** + * Test for a file selected together with an open project: Close Project must + * be both present and enabled. Regression test for the bug where + * selectionIsOfType(PROJECT) disabled the action for any mixed selection. + * + * @throws CoreException + */ + @Test + public void testFillContextMenu_fileAndOpenProjectSelection_closeProjectEnabled() throws CoreException { + // _p1 is already open; _project has a known 'src' folder + files + IProject openProj = ResourcesPlugin.getWorkspace().getRoot().getProject("Test"); + openProj.open(null); + // Select a file alongside a project (the typical Ctrl+A expanded scenario) + ResourceMgmtActionProvider provider = providerForObjects(_p1, openProj.getFile(".project")); + provider.fillContextMenu(manager); + assertTrue(menuHasContribution("org.eclipse.ui.CloseResourceAction"), + "Close Project should be in the menu"); + assertTrue(isMenuContributionEnabled("org.eclipse.ui.CloseResourceAction"), + "Close Project should be enabled when open projects are in the selection"); + assertTrue(menuHasContribution("org.eclipse.ui.CloseUnrelatedProjectsAction"), + "Close Unrelated Projects should be in the menu"); + assertTrue(isMenuContributionEnabled("org.eclipse.ui.CloseUnrelatedProjectsAction"), + "Close Unrelated Projects should be enabled when open projects are in the selection"); + } + + /** + * Test for mixed selection: an open project alongside a non-adaptable element + * (e.g. a working set header from Ctrl+A in Project Explorer). Close Project + * and Refresh must still appear — regression test for issue #3790. + * + * @throws CoreException + */ + @Test + public void testFillContextMenu_mixedSelectionOpenProjectAndNonAdaptableElement() throws CoreException { + IProject openProj = ResourcesPlugin.getWorkspace().getRoot().getProject("Test"); + openProj.open(null); + // Plain Object does not implement IAdaptable, so it is never resolved to a + // project — it counts as a non-project element in the selection. + Object nonResource = new Object(); + ResourceMgmtActionProvider provider = providerForObjects(openProj, nonResource); + provider.fillContextMenu(manager); + checkMenuHasCorrectContributions(false, true, false, true, true); + } + + /** + * Test for a fully expanded selection: two open projects plus child resources + * from both (simulating Ctrl+A when both projects are expanded). Close Project + * must still appear for the open projects in the selection. + * + * @throws CoreException + */ + @Test + public void testFillContextMenu_twoOpenProjectsWithChildResourcesSelection() throws CoreException { + // _p1 and _p2 are already opened in setUp() + IFolder srcFolder = _project.getFolder("src"); + IFolder binFolder = _project.getFolder("bin"); + ResourceMgmtActionProvider provider = providerForObjects(_p1, _p2, srcFolder, binFolder); + provider.fillContextMenu(manager); + checkMenuHasCorrectContributions(false, true, false, true, true); + } + /** * Test for 'open project' that doesn't have a builder attached - only 'open * project' should be disabled @@ -158,6 +219,19 @@ public void testFillContextMenu_openProjectWithBuilderSelection() throws CoreExc } } + /* + * Return a provider for a mixed/arbitrary selection (Object[]) + */ + private ResourceMgmtActionProvider providerForObjects(Object... selectedElements) { + ICommonActionExtensionSite cfg = new CommonActionExtensionSite("NA", "NA", + CommonViewerSiteFactory.createCommonViewerSite(_commonNavigator.getViewSite()), + (NavigatorContentService) _contentService, _viewer); + ResourceMgmtActionProvider provider = new ResourceMgmtActionProvider(); + provider.setContext(new ActionContext(new StructuredSelection(selectedElements))); + provider.init(cfg); + return provider; + } + /* * Return a provider, given the selected navigator items */ @@ -206,4 +280,16 @@ private boolean menuHasContribution(String contribution) { return false; } + /* + * Check whether the named menu entry is enabled + */ + private boolean isMenuContributionEnabled(String contribution) { + for (IContributionItem thisItem : manager.getItems()) { + if (thisItem.getId() != null && thisItem.getId().equals(contribution)) { + return thisItem.isEnabled(); + } + } + return false; + } + } From ea62040ff3d3a02213c84c95776d9ef8945ec2a1 Mon Sep 17 00:00:00 2001 From: Lars Vogel Date: Wed, 15 Apr 2026 17:11:25 +0200 Subject: [PATCH 2/2] Filter to projects in action classes instead of anonymous overrides Move the project-filtering logic from anonymous getSelectedResources() overrides in ResourceMgmtActionProvider into the action classes themselves (OpenResourceAction, CloseResourceAction, CloseUnrelatedProjectsAction). This fixes the API analysis build failure caused by illegally subclassing these action classes. The updateSelection() methods now filter getSelectedResources() to IProject instances, so mixed selections (e.g. Ctrl+A) correctly enable/disable Open/Close Project actions. The resourceChanged() methods also use stream-based project checks instead of selectionIsOfType() to react properly to project state changes during mixed selections. --- .../ui/actions/CloseResourceAction.java | 12 ++++----- .../actions/CloseUnrelatedProjectsAction.java | 2 +- .../ui/actions/OpenResourceAction.java | 11 ++++---- .../actions/ResourceMgmtActionProvider.java | 25 +++---------------- 4 files changed, 15 insertions(+), 35 deletions(-) diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java index 5395229474d..a11209a453b 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java @@ -16,7 +16,6 @@ package org.eclipse.ui.actions; import java.util.ArrayList; -import java.util.Iterator; import java.util.List; import org.eclipse.core.resources.IFile; @@ -229,15 +228,14 @@ protected boolean updateSelection(IStructuredSelection s) { // don't call super since we want to enable if open project is selected. setText(defaultText); setToolTipText(defaultToolTip); - List selectedResources = getSelectedResources(); - if (selectedResources.isEmpty() || selectedResources.stream().anyMatch(r -> !(r instanceof IProject))) { + List projects = getSelectedResources().stream() + .filter(IProject.class::isInstance).map(IProject.class::cast).toList(); + if (projects.isEmpty()) { return false; } boolean hasOpenProjects = false; - Iterator resources = selectedResources.iterator(); - while (resources.hasNext()) { - IProject currentResource = (IProject) resources.next(); + for (IProject currentResource : projects) { if (currentResource.isOpen()) { if (hasOpenProjects) { setText(pluralText); @@ -259,7 +257,7 @@ public synchronized void resourceChanged(IResourceChangeEvent event) { // Warning: code duplicated in OpenResourceAction List sel = getSelectedResources(); // don't bother looking at delta if selection not applicable - if (selectionIsOfType(IResource.PROJECT)) { + if (sel.stream().anyMatch(IProject.class::isInstance)) { IResourceDelta delta = event.getDelta(); if (delta != null) { IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED); diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java index 4a698a2bc33..711066642d9 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java @@ -262,7 +262,7 @@ protected List getSelectedResources() { public void resourceChanged(IResourceChangeEvent event) { // don't bother looking at delta if selection not applicable List selectedResources = super.getSelectedResources(); - if (!selectedResources.isEmpty() && selectedResources.stream().allMatch(r -> r instanceof IProject)) { + if (selectedResources.stream().anyMatch(IProject.class::isInstance)) { IResourceDelta delta = event.getDelta(); if (delta != null) { IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED); diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java index b22887b5c24..635fcbe7830 100644 --- a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java @@ -190,7 +190,7 @@ public void resourceChanged(IResourceChangeEvent event) { // Warning: code duplicated in CloseResourceAction List sel = getSelectedResources(); // don't bother looking at delta if selection not applicable - if (selectionIsOfType(IResource.PROJECT)) { + if (sel.stream().anyMatch(IProject.class::isInstance)) { IResourceDelta delta = event.getDelta(); if (delta != null) { IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED); @@ -304,14 +304,15 @@ protected boolean updateSelection(IStructuredSelection s) { // selected. setText(IDEWorkbenchMessages.OpenResourceAction_text); setToolTipText(IDEWorkbenchMessages.OpenResourceAction_toolTip); - List selectedResources = getSelectedResources(); - if (selectedResources.isEmpty() || selectedResources.stream().anyMatch(r -> !(r instanceof IProject))) { + List projects = getSelectedResources().stream() + .filter(IProject.class::isInstance).map(IProject.class::cast).toList(); + if (projects.isEmpty()) { return false; } boolean hasClosedProjects = false; - for (IResource currentResource : selectedResources) { - if (!((IProject) currentResource).isOpen()) { + for (IProject currentResource : projects) { + if (!currentResource.isOpen()) { if (hasClosedProjects) { setText(IDEWorkbenchMessages.OpenResourceAction_text_plural); setToolTipText(IDEWorkbenchMessages.OpenResourceAction_toolTip_plural); diff --git a/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java index 0a6e6bd6ddf..28111551293 100644 --- a/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java +++ b/bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java @@ -22,7 +22,6 @@ import org.eclipse.core.resources.ICommand; import org.eclipse.core.resources.IProject; -import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.WorkspaceJob; @@ -208,29 +207,11 @@ boolean hasBuilder(IProject project) { protected void makeActions() { IShellProvider sp = () -> shell; - openProjectAction = new OpenResourceAction(sp) { - @Override - protected synchronized List getSelectedResources() { - return super.getSelectedResources().stream() - .filter(IProject.class::isInstance).toList(); - } - }; + openProjectAction = new OpenResourceAction(sp); - closeProjectAction = new CloseResourceAction(sp) { - @Override - protected synchronized List getSelectedResources() { - return super.getSelectedResources().stream() - .filter(IProject.class::isInstance).toList(); - } - }; + closeProjectAction = new CloseResourceAction(sp); - closeUnrelatedProjectsAction = new CloseUnrelatedProjectsAction(sp) { - @Override - protected List getSelectedResources() { - return super.getSelectedResources().stream() - .filter(IProject.class::isInstance).toList(); - } - }; + closeUnrelatedProjectsAction = new CloseUnrelatedProjectsAction(sp); refreshAction = new RefreshAction(sp) { @Override