Skip to content

Commit e1e0229

Browse files
committed
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 <or/> 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 #3790
1 parent f22f75c commit e1e0229

File tree

6 files changed

+118
-15
lines changed

6 files changed

+118
-15
lines changed

bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseResourceAction.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,12 +229,13 @@ protected boolean updateSelection(IStructuredSelection s) {
229229
// don't call super since we want to enable if open project is selected.
230230
setText(defaultText);
231231
setToolTipText(defaultToolTip);
232-
if (!selectionIsOfType(IResource.PROJECT)) {
232+
List<? extends IResource> selectedResources = getSelectedResources();
233+
if (selectedResources.isEmpty() || selectedResources.stream().anyMatch(r -> !(r instanceof IProject))) {
233234
return false;
234235
}
235236

236237
boolean hasOpenProjects = false;
237-
Iterator<? extends IResource> resources = getSelectedResources().iterator();
238+
Iterator<? extends IResource> resources = selectedResources.iterator();
238239
while (resources.hasNext()) {
239240
IProject currentResource = (IProject) resources.next();
240241
if (currentResource.isOpen()) {

bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/CloseUnrelatedProjectsAction.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,8 @@ protected List<? extends IResource> getSelectedResources() {
261261
@Override
262262
public void resourceChanged(IResourceChangeEvent event) {
263263
// don't bother looking at delta if selection not applicable
264-
if (selectionIsOfType(IResource.PROJECT)) {
264+
List<? extends IResource> selectedResources = super.getSelectedResources();
265+
if (!selectedResources.isEmpty() && selectedResources.stream().allMatch(r -> r instanceof IProject)) {
265266
IResourceDelta delta = event.getDelta();
266267
if (delta != null) {
267268
IResourceDelta[] projDeltas = delta.getAffectedChildren(IResourceDelta.CHANGED);

bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/OpenResourceAction.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,12 +304,13 @@ protected boolean updateSelection(IStructuredSelection s) {
304304
// selected.
305305
setText(IDEWorkbenchMessages.OpenResourceAction_text);
306306
setToolTipText(IDEWorkbenchMessages.OpenResourceAction_toolTip);
307-
if (!selectionIsOfType(IResource.PROJECT)) {
307+
List<? extends IResource> selectedResources = getSelectedResources();
308+
if (selectedResources.isEmpty() || selectedResources.stream().anyMatch(r -> !(r instanceof IProject))) {
308309
return false;
309310
}
310311

311312
boolean hasClosedProjects = false;
312-
for (IResource currentResource : getSelectedResources()) {
313+
for (IResource currentResource : selectedResources) {
313314
if (!((IProject) currentResource).isOpen()) {
314315
if (hasClosedProjects) {
315316
setText(IDEWorkbenchMessages.OpenResourceAction_text_plural);

bundles/org.eclipse.ui.navigator.resources/plugin.xml

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -237,13 +237,8 @@
237237
class="org.eclipse.ui.internal.navigator.resources.actions.ResourceMgmtActionProvider"
238238
id="org.eclipse.ui.navigator.resources.ResourceMgmtActions">
239239
<enablement>
240-
<or>
241-
<adapt type="org.eclipse.core.resources.IResource" />
242-
<adapt type="java.util.Collection">
243-
<count value="0" />
244-
</adapt>
245-
<adapt type="org.eclipse.ui.IWorkingSet" />
246-
</or>
240+
<!-- Always enabled: fillContextMenu() filters to applicable projects -->
241+
<or/>
247242
</enablement>
248243
</actionProvider>
249244

bundles/org.eclipse.ui.navigator.resources/src/org/eclipse/ui/internal/navigator/resources/actions/ResourceMgmtActionProvider.java

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222

2323
import org.eclipse.core.resources.ICommand;
2424
import org.eclipse.core.resources.IProject;
25+
import org.eclipse.core.resources.IResource;
2526
import org.eclipse.core.resources.IncrementalProjectBuilder;
2627
import org.eclipse.core.resources.ResourcesPlugin;
2728
import org.eclipse.core.resources.WorkspaceJob;
@@ -207,11 +208,29 @@ boolean hasBuilder(IProject project) {
207208
protected void makeActions() {
208209
IShellProvider sp = () -> shell;
209210

210-
openProjectAction = new OpenResourceAction(sp);
211+
openProjectAction = new OpenResourceAction(sp) {
212+
@Override
213+
protected synchronized List<? extends IResource> getSelectedResources() {
214+
return super.getSelectedResources().stream()
215+
.filter(IProject.class::isInstance).toList();
216+
}
217+
};
211218

212-
closeProjectAction = new CloseResourceAction(sp);
219+
closeProjectAction = new CloseResourceAction(sp) {
220+
@Override
221+
protected synchronized List<? extends IResource> getSelectedResources() {
222+
return super.getSelectedResources().stream()
223+
.filter(IProject.class::isInstance).toList();
224+
}
225+
};
213226

214-
closeUnrelatedProjectsAction = new CloseUnrelatedProjectsAction(sp);
227+
closeUnrelatedProjectsAction = new CloseUnrelatedProjectsAction(sp) {
228+
@Override
229+
protected List<? extends IResource> getSelectedResources() {
230+
return super.getSelectedResources().stream()
231+
.filter(IProject.class::isInstance).toList();
232+
}
233+
};
215234

216235
refreshAction = new RefreshAction(sp) {
217236
@Override

tests/org.eclipse.ui.tests.navigator/src/org/eclipse/ui/tests/navigator/resources/ResourceMgmtActionProviderTests.java

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,67 @@ public void testFillContextMenu_openProjectNoBuilderSelection() throws CoreExcep
118118
}
119119
}
120120

121+
/**
122+
* Test for a file selected together with an open project: Close Project must
123+
* be both present and enabled. Regression test for the bug where
124+
* selectionIsOfType(PROJECT) disabled the action for any mixed selection.
125+
*
126+
* @throws CoreException
127+
*/
128+
@Test
129+
public void testFillContextMenu_fileAndOpenProjectSelection_closeProjectEnabled() throws CoreException {
130+
// _p1 is already open; _project has a known 'src' folder + files
131+
IProject openProj = ResourcesPlugin.getWorkspace().getRoot().getProject("Test");
132+
openProj.open(null);
133+
// Select a file alongside a project (the typical Ctrl+A expanded scenario)
134+
ResourceMgmtActionProvider provider = providerForObjects(_p1, openProj.getFile(".project"));
135+
provider.fillContextMenu(manager);
136+
assertTrue(menuHasContribution("org.eclipse.ui.CloseResourceAction"),
137+
"Close Project should be in the menu");
138+
assertTrue(isMenuContributionEnabled("org.eclipse.ui.CloseResourceAction"),
139+
"Close Project should be enabled when open projects are in the selection");
140+
assertTrue(menuHasContribution("org.eclipse.ui.CloseUnrelatedProjectsAction"),
141+
"Close Unrelated Projects should be in the menu");
142+
assertTrue(isMenuContributionEnabled("org.eclipse.ui.CloseUnrelatedProjectsAction"),
143+
"Close Unrelated Projects should be enabled when open projects are in the selection");
144+
}
145+
146+
/**
147+
* Test for mixed selection: an open project alongside a non-adaptable element
148+
* (e.g. a working set header from Ctrl+A in Project Explorer). Close Project
149+
* and Refresh must still appear — regression test for issue #3790.
150+
*
151+
* @throws CoreException
152+
*/
153+
@Test
154+
public void testFillContextMenu_mixedSelectionOpenProjectAndNonAdaptableElement() throws CoreException {
155+
IProject openProj = ResourcesPlugin.getWorkspace().getRoot().getProject("Test");
156+
openProj.open(null);
157+
// Plain Object does not implement IAdaptable, so it is never resolved to a
158+
// project — it counts as a non-project element in the selection.
159+
Object nonResource = new Object();
160+
ResourceMgmtActionProvider provider = providerForObjects(openProj, nonResource);
161+
provider.fillContextMenu(manager);
162+
checkMenuHasCorrectContributions(false, true, false, true, true);
163+
}
164+
165+
/**
166+
* Test for a fully expanded selection: two open projects plus child resources
167+
* from both (simulating Ctrl+A when both projects are expanded). Close Project
168+
* must still appear for the open projects in the selection.
169+
*
170+
* @throws CoreException
171+
*/
172+
@Test
173+
public void testFillContextMenu_twoOpenProjectsWithChildResourcesSelection() throws CoreException {
174+
// _p1 and _p2 are already opened in setUp()
175+
IFolder srcFolder = _project.getFolder("src");
176+
IFolder binFolder = _project.getFolder("bin");
177+
ResourceMgmtActionProvider provider = providerForObjects(_p1, _p2, srcFolder, binFolder);
178+
provider.fillContextMenu(manager);
179+
checkMenuHasCorrectContributions(false, true, false, true, true);
180+
}
181+
121182
/**
122183
* Test for 'open project' that doesn't have a builder attached - only 'open
123184
* project' should be disabled
@@ -158,6 +219,19 @@ public void testFillContextMenu_openProjectWithBuilderSelection() throws CoreExc
158219
}
159220
}
160221

222+
/*
223+
* Return a provider for a mixed/arbitrary selection (Object[])
224+
*/
225+
private ResourceMgmtActionProvider providerForObjects(Object... selectedElements) {
226+
ICommonActionExtensionSite cfg = new CommonActionExtensionSite("NA", "NA",
227+
CommonViewerSiteFactory.createCommonViewerSite(_commonNavigator.getViewSite()),
228+
(NavigatorContentService) _contentService, _viewer);
229+
ResourceMgmtActionProvider provider = new ResourceMgmtActionProvider();
230+
provider.setContext(new ActionContext(new StructuredSelection(selectedElements)));
231+
provider.init(cfg);
232+
return provider;
233+
}
234+
161235
/*
162236
* Return a provider, given the selected navigator items
163237
*/
@@ -206,4 +280,16 @@ private boolean menuHasContribution(String contribution) {
206280
return false;
207281
}
208282

283+
/*
284+
* Check whether the named menu entry is enabled
285+
*/
286+
private boolean isMenuContributionEnabled(String contribution) {
287+
for (IContributionItem thisItem : manager.getItems()) {
288+
if (thisItem.getId() != null && thisItem.getId().equals(contribution)) {
289+
return thisItem.isEnabled();
290+
}
291+
}
292+
return false;
293+
}
294+
209295
}

0 commit comments

Comments
 (0)