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..be2cbe3693d 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2017 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -12,6 +12,7 @@ * IBM Corporation - initial API and implementation * Andrey Loskutov - Bug 41431, 462760, 461786 * Lucas Bullen (Red Hat Inc.) - Bug 522096 - "Close Projects" on working set + * Lars Vogel - ask before closing nested projects *******************************************************************************/ package org.eclipse.ui.actions; @@ -35,6 +36,7 @@ import org.eclipse.core.runtime.SafeRunner; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.MultiRule; +import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.util.SafeRunnable; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.window.IShellProvider; @@ -48,7 +50,9 @@ import org.eclipse.ui.PartInitException; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.ide.IDE; +import org.eclipse.ui.internal.ide.IDEInternalPreferences; import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; +import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; import org.eclipse.ui.internal.ide.IIDEHelpContextIds; /** @@ -71,6 +75,11 @@ public class CloseResourceAction extends WorkspaceAction implements IResourceCha private String[] modelProviderIds; + /** Projects the last {@link #run()} decided to close, may be wider than the selection. */ + private List resourcesToClose; + + private boolean promptForNestedProjects = true; + /** * Creates a new action. * @@ -179,6 +188,7 @@ protected void invokeOperation(IResource resource, IProgressMonitor monitor) thr */ @Override public void run() { + resourcesToClose = null; // Get the items to close. List projects = getSelectedResources(); if (projects == null || projects.isEmpty()) { @@ -186,6 +196,20 @@ public void run() { return; } + projects = promptForProjectsToClose(projects); + if (projects == null) { + // the user cancelled the operation + return; + } + resourcesToClose = projects; + try { + closeProjects(projects); + } finally { + resourcesToClose = null; + } + } + + private void closeProjects(List projects) { final IResource[] projectArray = projects.toArray(new IResource[projects.size()]); if (!IDE.saveAllEditors(projectArray, true)) { @@ -219,6 +243,89 @@ protected boolean shouldPerformResourcePruning() { return false; } + @Override + protected List getActionResources() { + if (resourcesToClose != null) { + return resourcesToClose; + } + return super.getActionResources(); + } + + /** + * Offers to close open projects nested below the selected ones, unless the + * preference already decides. + * + * @return the projects to close, or null if the user cancelled + */ + private List promptForProjectsToClose(List projects) { + if (!promptForRelatedProjects()) { + return projects; + } + List nestedProjects = NestedProjects.below(projects, true); + if (nestedProjects.isEmpty()) { + return projects; + } + IPreferenceStore store = IDEWorkbenchPlugin.getDefault().getPreferenceStore(); + String key = IDEInternalPreferences.CLOSE_NESTED_PROJECTS; + String value = store.getString(key); + if (IDEInternalPreferences.PSPM_NEVER.equals(value)) { + return projects; + } + if (!IDEInternalPreferences.PSPM_ALWAYS.equals(value)) { + RelatedProjectsDialog.Answer answer = RelatedProjectsDialog.open(getShell(), + IDEWorkbenchMessages.CloseResourceAction_promptTitle, nestedMessage(projects, nestedProjects), + IDEWorkbenchMessages.CloseResourceAction_close, store, key, null); + if (answer == null) { + return null; + } + if (!answer.includeNested()) { + return projects; + } + } + List allProjects = new ArrayList<>(projects); + allProjects.addAll(nestedProjects); + return allProjects; + } + + /** + * @return the question asked when the selection nests further open projects + */ + private static String nestedMessage(List projects, List nestedProjects) { + boolean oneProject = projects.size() == 1; + if (nestedProjects.size() == 1) { + return oneProject + ? NLS.bind(IDEWorkbenchMessages.CloseResourceAction_closeOneNestedBelowProject, + projects.get(0).getName()) + : IDEWorkbenchMessages.CloseResourceAction_closeOneNestedBelowSelection; + } + Integer count = Integer.valueOf(nestedProjects.size()); + return oneProject + ? NLS.bind(IDEWorkbenchMessages.CloseResourceAction_closeNestedBelowProject, count, + projects.get(0).getName()) + : NLS.bind(IDEWorkbenchMessages.CloseResourceAction_closeNestedBelowSelection, count); + } + + /** + * Sets whether {@link #run()} also handles open projects nested below the + * selected ones, as the workspace preference says or by asking. Turned off, + * the action closes exactly the selected projects. + * + * @param prompt true to handle nested projects, the default + * @since 3.24 + */ + public void setPromptForNestedProjects(boolean prompt) { + promptForNestedProjects = prompt; + } + + /** + * @return true to ask about projects implied by the selection but + * not part of it. Subclasses computing and confirming the projects to + * close themselves answer false. + */ + boolean promptForRelatedProjects() { + return promptForNestedProjects; + } + /** * The CloseResourceAction implementation of this * SelectionListenerAction method ensures that this action is 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 65f94ba9a21..5f6abf13f52 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2006, 2017 IBM Corporation and others. + * Copyright (c) 2006, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -14,6 +14,7 @@ * Andrey Loskutov - generified interface, bug 462760 * Mickael Istria (Red Hat Inc.) - Bug 486901 * Lucas Bullen (Red Hat Inc.) - Bug 522096 - "Close Projects" on working set + * Lars Vogel - ask before closing nested projects *******************************************************************************/ package org.eclipse.ui.actions; @@ -113,6 +114,15 @@ public void run() { } } + /** + * The projects to close come from the project graph, not from the selection, + * and {@link #promptForConfirmation()} already confirms them. + */ + @Override + boolean promptForRelatedProjects() { + return false; + } + /** * Rebuilds the project graph before the projects to close are computed from it. * Enablement may answer from a stale graph, but closing projects should not. A diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/NestedProjects.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/NestedProjects.java new file mode 100644 index 00000000000..68da05c684c --- /dev/null +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/NestedProjects.java @@ -0,0 +1,72 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel 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.ui.actions; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IResource; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.IPath; + +/** + * Finds projects located inside other projects, such as Maven or Gradle + * modules below their root. + */ +final class NestedProjects { + + private NestedProjects() { + } + + /** + * Returns the open or closed projects located inside the given ones but not + * among them, parents before their children. + */ + static List below(List projects, boolean open) { + List locations = new ArrayList<>(projects.size()); + for (IResource project : projects) { + IPath location = project.getLocation(); + if (location != null) { + locations.add(location); + } + } + if (locations.isEmpty()) { + return Collections.emptyList(); + } + Set selected = new HashSet<>(projects); + List nestedProjects = new ArrayList<>(); + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + if (project.isOpen() != open || selected.contains(project)) { + continue; + } + IPath location = project.getLocation(); + if (location == null) { + continue; + } + for (IPath selectedLocation : locations) { + if (!selectedLocation.equals(location) && selectedLocation.isPrefixOf(location)) { + nestedProjects.add(project); + break; + } + } + } + nestedProjects.sort(Comparator.comparingInt(project -> project.getLocation().segmentCount())); + return nestedProjects; + } +} 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 dd0a8693885..e192f13b125 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 @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2000, 2015 IBM Corporation and others. + * Copyright (c) 2000, 2026 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -12,35 +12,37 @@ * IBM Corporation - initial API and implementation * Mohamed Tarief , IBM - Bug 139211 * Lucas Bullen (Red Hat Inc.) - Bug 522096 - "Close Projects" on working set + * Lars Vogel - ask before opening nested projects *******************************************************************************/ package org.eclipse.ui.actions; import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; +import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.WorkspaceJob; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.ILog; +import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; -import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.SubMonitor; import org.eclipse.core.runtime.jobs.Job; -import org.eclipse.jface.dialogs.IDialogConstants; -import org.eclipse.jface.dialogs.MessageDialogWithToggle; import org.eclipse.jface.preference.IPreferenceStore; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.window.IShellProvider; -import org.eclipse.jface.window.Window; -import org.eclipse.swt.SWT; -import org.eclipse.swt.widgets.Display; +import org.eclipse.osgi.util.NLS; import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.PlatformUI; import org.eclipse.ui.internal.ide.IDEInternalPreferences; @@ -66,6 +68,8 @@ public class OpenResourceAction extends WorkspaceAction implements IResourceChan */ public static final String ID = PlatformUI.PLUGIN_ID + ".OpenResourceAction"; //$NON-NLS-1$ + private boolean promptForNestedProjects = true; + /** * Creates a new action. * @@ -133,59 +137,11 @@ protected String getProblemsTitle() { return IDEWorkbenchMessages.OpenResourceAction_dialogTitle; } - /** - * Returns whether there are closed projects in the workspace that are - * not part of the current selection. - */ - private boolean hasOtherClosedProjects() { - //count the closed projects in the selection - int closedInSelection = 0; - for (IResource project : getSelectedResources()) { - if (!((IProject) project).isOpen()) { - closedInSelection++; - } - } - //there are other closed projects if the selection does - //not contain all closed projects in the workspace - return closedInSelection < countClosedProjects(); - } - @Override protected void invokeOperation(IResource resource, IProgressMonitor monitor) throws CoreException { ((IProject) resource).open(IResource.BACKGROUND_REFRESH, monitor); } - /** - * Returns the preference for whether to open required projects when opening - * a project. Consults the preference and prompts the user if necessary. - * - * @return true if referenced projects should be opened, and - * false otherwise. - */ - private boolean promptToOpenWithReferences() { - IPreferenceStore store = IDEWorkbenchPlugin.getDefault().getPreferenceStore(); - String key = IDEInternalPreferences.OPEN_REQUIRED_PROJECTS; - String value = store.getString(key); - if (MessageDialogWithToggle.ALWAYS.equals(value)) { - return true; - } - if (MessageDialogWithToggle.NEVER.equals(value)) { - return false; - } - String message = IDEWorkbenchMessages.OpenResourceAction_openRequiredProjects; - MessageDialogWithToggle dialog = MessageDialogWithToggle.openYesNoQuestion(getShell(), IDEWorkbenchMessages.Question, message, null, false, store, key); - int result = dialog.getReturnCode(); - // the result is equal to SWT.DEFAULT if the user uses the 'esc' key to close the dialog - if (result == Window.CANCEL || result == SWT.DEFAULT) { - throw new OperationCanceledException(); - } - return dialog.getReturnCode() == IDialogConstants.YES_ID; - } - - /** - * Handles a resource changed event by updating the enablement if one of the - * selected projects is opened or closed. - */ @Override public void resourceChanged(IResourceChangeEvent event) { // Warning: code duplicated in CloseResourceAction @@ -209,11 +165,82 @@ public void resourceChanged(IResourceChangeEvent event) { @Override public void run() { - try { - runOpenWithReferences(); - } catch (OperationCanceledException e) { - //just return when canceled + List projects = getActionResources(); + List nestedProjects = promptForNestedProjects ? NestedProjects.below(projects, false) + : Collections.emptyList(); + IPreferenceStore store = IDEWorkbenchPlugin.getDefault().getPreferenceStore(); + String nestedValue = store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS); + String referencedValue = store.getString(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS); + boolean includeNested = IDEInternalPreferences.PSPM_ALWAYS.equals(nestedValue); + boolean includeReferenced = IDEInternalPreferences.PSPM_ALWAYS.equals(referencedValue); + boolean askNested = !nestedProjects.isEmpty() && prompts(nestedValue); + boolean askReferenced = prompts(referencedValue) && hasClosedReferences(projects, nestedProjects); + if (askNested || askReferenced) { + RelatedProjectsDialog.Answer answer = RelatedProjectsDialog.open(getShell(), + IDEWorkbenchMessages.OpenResourceAction_promptTitle, + askNested ? nestedMessage(projects, nestedProjects) + : IDEWorkbenchMessages.OpenResourceAction_referencedProjectsClosed, + IDEWorkbenchMessages.OpenResourceAction_open, store, + askNested ? IDEInternalPreferences.OPEN_NESTED_PROJECTS : null, + askReferenced ? IDEInternalPreferences.OPEN_REQUIRED_PROJECTS : null); + if (answer == null) { + return; + } + includeNested = askNested ? answer.includeNested() : includeNested; + includeReferenced = askReferenced ? answer.includeReferenced() : includeReferenced; + } + List allProjects = new ArrayList<>(projects); + if (includeNested) { + allProjects.addAll(nestedProjects); + } + runOpenWithReferences(allProjects, includeReferenced); + } + + /** + * Sets whether {@link #run()} also handles closed projects nested below the + * selected ones, as the workspace preference says or by asking. Turned off, + * the action opens exactly the selected projects, with their references as + * before. + * + * @param prompt true to handle nested projects, the default + * @since 3.24 + */ + public void setPromptForNestedProjects(boolean prompt) { + promptForNestedProjects = prompt; + } + + private static boolean prompts(String preferenceValue) { + return !IDEInternalPreferences.PSPM_ALWAYS.equals(preferenceValue) + && !IDEInternalPreferences.PSPM_NEVER.equals(preferenceValue); + } + + /** + * Returns whether a project among the given ones references a closed + * project outside them. Closed projects cannot be asked, so the references + * come from their .project files. + */ + private static boolean hasClosedReferences(List projects, List nestedProjects) { + Set candidates = new LinkedHashSet<>(projects); + candidates.addAll(nestedProjects); + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + for (IResource candidate : candidates) { + IPath location = candidate.getLocation(); + if (!(candidate instanceof IProject project) || project.isOpen() || location == null) { + continue; + } + IProjectDescription description; + try { + description = workspace.loadProjectDescription(location.append(IProjectDescription.DESCRIPTION_FILE_NAME)); + } catch (CoreException e) { + continue; + } + for (IProject reference : description.getReferencedProjects()) { + if (reference.exists() && !reference.isOpen() && !candidates.contains(reference)) { + return true; + } + } } + return false; } /** @@ -225,14 +252,30 @@ private static void logOpenFailure(IProject project, CoreException e) { } /** - * Opens the selected projects, and all related projects, in the background. + * @return the statement shown when the selection nests further closed projects + */ + private static String nestedMessage(List projects, List nestedProjects) { + boolean oneProject = projects.size() == 1; + if (nestedProjects.size() == 1) { + return oneProject + ? NLS.bind(IDEWorkbenchMessages.OpenResourceAction_openOneNestedBelowProject, + projects.get(0).getName()) + : IDEWorkbenchMessages.OpenResourceAction_openOneNestedBelowSelection; + } + Integer count = Integer.valueOf(nestedProjects.size()); + return oneProject + ? NLS.bind(IDEWorkbenchMessages.OpenResourceAction_openNestedBelowProject, count, + projects.get(0).getName()) + : NLS.bind(IDEWorkbenchMessages.OpenResourceAction_openNestedBelowSelection, count); + } + + /** + * Opens the given projects in the background, with the projects they + * reference if wanted. */ - private void runOpenWithReferences() { - final List resources = new ArrayList<>(getActionResources()); + private void runOpenWithReferences(List projects, boolean openProjectReferences) { + final List resources = new ArrayList<>(projects); Job job = new WorkspaceJob(removeMnemonics(getText())) { - private boolean openProjectReferences = true; - private boolean hasPrompted = false; - private boolean canceled = false; /** * Opens a project along with all projects it references */ @@ -247,31 +290,8 @@ private void doOpenWithReferences(IProject project, IProgressMonitor mon) throws logOpenFailure(project, e); return; } - final IProject[] references = project.getReferencedProjects(); - if (!hasPrompted) { - openProjectReferences = false; - for (IProject reference : references) { - if (reference.exists() && !reference.isOpen()) { - openProjectReferences = true; - break; - } - } - if (openProjectReferences && hasOtherClosedProjects()) { - Display.getDefault().syncExec(() -> { - try { - openProjectReferences = promptToOpenWithReferences(); - } catch (OperationCanceledException e) { - canceled = true; - } - //remember that we have prompted to avoid repeating the analysis - hasPrompted = true; - }); - if (canceled) { - throw new OperationCanceledException(); - } - } - } if (openProjectReferences) { + IProject[] references = project.getReferencedProjects(); SubMonitor loopMonitor = subMonitor.split(1).setWorkRemaining(references.length); for (IProject reference : references) { doOpenWithReferences(reference, loopMonitor.split(1)); diff --git a/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RelatedProjectsDialog.java b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RelatedProjectsDialog.java new file mode 100644 index 00000000000..bcda8ec5c76 --- /dev/null +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RelatedProjectsDialog.java @@ -0,0 +1,121 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel 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.ui.actions; + +import java.util.LinkedHashMap; + +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.dialogs.MessageDialogWithToggle; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.layout.GridLayoutFactory; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; + +/** + * Asks whether to include the projects nested below or referenced by the + * selection, with a toggle that stores the answers in preferences. + */ +final class RelatedProjectsDialog extends MessageDialogWithToggle { + + /** The answer, each flag only meaningful when its check box was shown. */ + record Answer(boolean includeNested, boolean includeReferenced) { + } + + private final IPreferenceStore store; + private final String nestedKey; + private final String referencedKey; + private Button nestedButton; + private Button referencedButton; + private Answer answer; + + private RelatedProjectsDialog(Shell parent, String title, String message, String actionLabel, + IPreferenceStore store, String nestedKey, String referencedKey) { + super(parent, title, null, message, QUESTION, buttons(actionLabel), 0, null, false); + setShellStyle(getShellStyle() | SWT.SHEET); + this.store = store; + this.nestedKey = nestedKey; + this.referencedKey = referencedKey; + } + + /** The map fixes the ids, a plain label array gets internal ids instead of OK_ID. */ + private static LinkedHashMap buttons(String actionLabel) { + LinkedHashMap buttons = new LinkedHashMap<>(); + buttons.put(actionLabel, Integer.valueOf(IDialogConstants.OK_ID)); + buttons.put(IDialogConstants.CANCEL_LABEL, Integer.valueOf(IDialogConstants.CANCEL_ID)); + return buttons; + } + + /** + * Opens the dialog with a check box for each non-null preference key and + * returns the answer, or null if the user cancelled. A checked + * toggle stores each answer under its key as {@link #ALWAYS} or + * {@link #NEVER}. + */ + static Answer open(Shell parent, String title, String message, String actionLabel, IPreferenceStore store, + String nestedKey, String referencedKey) { + RelatedProjectsDialog dialog = new RelatedProjectsDialog(parent, title, message, actionLabel, store, + nestedKey, referencedKey); + if (dialog.open() != IDialogConstants.OK_ID) { + return null; + } + return dialog.answer; + } + + @Override + protected Control createCustomArea(Composite parent) { + Composite area = new Composite(parent, SWT.NONE); + GridLayoutFactory.fillDefaults().applyTo(area); + GridDataFactory.fillDefaults().span(2, 1).applyTo(area); + if (nestedKey != null) { + nestedButton = createCheckBox(area, IDEWorkbenchMessages.RelatedProjectsDialog_includeNested); + nestedButton.setSelection(true); + } + if (referencedKey != null) { + referencedButton = createCheckBox(area, IDEWorkbenchMessages.RelatedProjectsDialog_includeReferenced); + } + return area; + } + + private static Button createCheckBox(Composite parent, String text) { + Button button = new Button(parent, SWT.CHECK | SWT.LEFT); + button.setText(text); + button.setFont(parent.getFont()); + return button; + } + + @Override + protected void buttonPressed(int buttonId) { + answer = new Answer(isChecked(nestedButton), isChecked(referencedButton)); + if (buttonId == IDialogConstants.OK_ID && getToggleState()) { + remember(nestedKey, answer.includeNested()); + remember(referencedKey, answer.includeReferenced()); + } + super.buttonPressed(buttonId); + } + + private static boolean isChecked(Button button) { + return button != null && button.getSelection(); + } + + private void remember(String key, boolean include) { + if (key != null) { + store.setValue(key, include ? ALWAYS : NEVER); + } + } +} diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java index 1e6e940802c..d2f33bc2f1f 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEInternalPreferences.java @@ -55,6 +55,16 @@ public interface IDEInternalPreferences { */ String CLOSE_UNRELATED_PROJECTS = "CLOSE_UNRELATED_PROJECTS"; //$NON-NLS-1$ + /** + * (String) Whether to open projects nested below a project that is opened. + */ + String OPEN_NESTED_PROJECTS = "OPEN_NESTED_PROJECTS"; //$NON-NLS-1$ + + /** + * (String) Whether to close projects nested below a project that is closed. + */ + String CLOSE_NESTED_PROJECTS = "CLOSE_NESTED_PROJECTS"; //$NON-NLS-1$ + String PSPM_PROMPT = MessageDialogWithToggle.PROMPT; String PSPM_ALWAYS = MessageDialogWithToggle.ALWAYS; diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java index 7978720f57e..201ae2f3067 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEPreferenceInitializer.java @@ -58,6 +58,8 @@ public void initializeDefaultPreferences() { node.put(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); node.putBoolean(IDEInternalPreferences.CLOSE_UNRELATED_PROJECTS, false); + node.put(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + node.put(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); node.putBoolean(IDEInternalPreferences.WARN_ABOUT_WORKSPACE_INCOMPATIBILITY, true); diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java index a5132dcef7d..28af01b5961 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/IDEWorkbenchMessages.java @@ -322,7 +322,13 @@ public class IDEWorkbenchMessages extends NLS { public static String OpenResourceAction_problemMessage; public static String OpenResourceAction_operationMessage; public static String OpenResourceAction_operationMessage_plural; - public static String OpenResourceAction_openRequiredProjects; + public static String OpenResourceAction_promptTitle; + public static String OpenResourceAction_referencedProjectsClosed; + public static String OpenResourceAction_openOneNestedBelowProject; + public static String OpenResourceAction_openNestedBelowProject; + public static String OpenResourceAction_openOneNestedBelowSelection; + public static String OpenResourceAction_openNestedBelowSelection; + public static String OpenResourceAction_open; public static String CloseResourceAction_text; public static String CloseResourceAction_text_plural; @@ -335,6 +341,14 @@ public class IDEWorkbenchMessages extends NLS { public static String CloseResourceAction_problemMessage; public static String CloseResourceAction_operationMessage; public static String CloseResourceAction_operationMessage_plural; + public static String CloseResourceAction_promptTitle; + public static String CloseResourceAction_closeOneNestedBelowProject; + public static String CloseResourceAction_closeNestedBelowProject; + public static String CloseResourceAction_closeOneNestedBelowSelection; + public static String CloseResourceAction_closeNestedBelowSelection; + public static String CloseResourceAction_close; + public static String RelatedProjectsDialog_includeNested; + public static String RelatedProjectsDialog_includeReferenced; public static String CloseUnrelatedProjectsAction_text; public static String CloseUnrelatedProjectsAction_text_plural; @@ -562,6 +576,8 @@ public class IDEWorkbenchMessages extends NLS { public static String IDEWorkspacePreference_otherLineDelim; public static String IDEWorkspacePreference_relatedLink; public static String IDEWorkspacePreference_openReferencedProjects; + public static String IDEWorkspacePreference_openNestedProjects; + public static String IDEWorkspacePreference_closeNestedProjects; public static String IDEWorkspacePreference_closeUnrelatedProjectsToolTip; public static String IDEWorkspacePreference_windowTitleGroupText; public static String IDEWorkspacePreference_showLocationInWindowTitle; diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java index c37d677330d..32ace946fea 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/dialogs/IDEWorkspacePreferencePage.java @@ -110,6 +110,10 @@ public class IDEWorkspacePreferencePage extends PreferencePage implements IWorkb private ComboFieldEditor openReferencesEditor; + private ComboFieldEditor openNestedEditor; + + private ComboFieldEditor closeNestedEditor; + private StringFieldEditor systemExplorer; private ComboFieldEditor missingNatureSeverityCombo; @@ -143,7 +147,12 @@ protected Control createContents(Composite parent) { createSpace(composite); Composite comboParent = new Composite(composite, SWT.NONE); comboParent.setLayout(new GridLayout(2, false)); - createOpenPrefControls(comboParent); + openReferencesEditor = createPromptPrefControls(comboParent, IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, + IDEWorkbenchMessages.IDEWorkspacePreference_openReferencedProjects); + openNestedEditor = createPromptPrefControls(comboParent, IDEInternalPreferences.OPEN_NESTED_PROJECTS, + IDEWorkbenchMessages.IDEWorkspacePreference_openNestedProjects); + closeNestedEditor = createPromptPrefControls(comboParent, IDEInternalPreferences.CLOSE_NESTED_PROJECTS, + IDEWorkbenchMessages.IDEWorkspacePreference_closeNestedProjects); createMissingNaturePref(comboParent); createMissingEncodingPref(comboParent); @@ -217,23 +226,18 @@ private void createMissingEncodingPref(Composite parent) { } /** - * Creates controls for the preference to open required projects when opening a - * project. - * - * @param parent - * The parent control + * Creates an Always/Never/Prompt combo for the given preference. */ - private void createOpenPrefControls(Composite parent) { - String name = IDEInternalPreferences.OPEN_REQUIRED_PROJECTS; - String label = IDEWorkbenchMessages.IDEWorkspacePreference_openReferencedProjects; + private ComboFieldEditor createPromptPrefControls(Composite parent, String name, String label) { String[][] namesAndValues = { { Action.removeMnemonics(IDEWorkbenchMessages.Always), IDEInternalPreferences.PSPM_ALWAYS }, { Action.removeMnemonics(IDEWorkbenchMessages.Never), IDEInternalPreferences.PSPM_NEVER }, { Action.removeMnemonics(IDEWorkbenchMessages.Prompt), IDEInternalPreferences.PSPM_PROMPT } }; - openReferencesEditor = new ComboFieldEditorInGrid(name, label, namesAndValues, parent); - openReferencesEditor.setPreferenceStore(getIDEPreferenceStore()); - openReferencesEditor.setPage(this); - openReferencesEditor.load(); + ComboFieldEditor editor = new ComboFieldEditorInGrid(name, label, namesAndValues, parent); + editor.setPreferenceStore(getIDEPreferenceStore()); + editor.setPage(this); + editor.load(); + return editor; } /** @@ -562,6 +566,8 @@ protected void performDefaults() { encodingEditor.loadDefault(); lineSeparatorEditor.loadDefault(); openReferencesEditor.loadDefault(); + openNestedEditor.loadDefault(); + closeNestedEditor.loadDefault(); missingNatureSeverityCombo.loadDefault(); missingEncodingSeverityCombo.loadDefault(); @@ -628,6 +634,8 @@ public boolean performOk() { encodingEditor.store(); lineSeparatorEditor.store(); openReferencesEditor.store(); + openNestedEditor.store(); + closeNestedEditor.store(); missingNatureSeverityCombo.store(); missingEncodingSeverityCombo.store(); diff --git a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties index f267fd94efb..85ba3216a06 100644 --- a/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties +++ b/bundles/org.eclipse.ui.ide/src/org/eclipse/ui/internal/ide/messages.properties @@ -281,7 +281,13 @@ OpenResourceAction_dialogTitle = Open Problems OpenResourceAction_problemMessage = Problems occurred opening the selected resources. OpenResourceAction_operationMessage = Opening project... OpenResourceAction_operationMessage_plural = Opening projects... -OpenResourceAction_openRequiredProjects = Should referenced projects also be opened where applicable? +OpenResourceAction_promptTitle = Open Project +OpenResourceAction_referencedProjectsClosed = The selected projects reference closed projects. +OpenResourceAction_openOneNestedBelowProject = ''{0}'' contains 1 closed nested project. +OpenResourceAction_openNestedBelowProject = ''{1}'' contains {0} closed nested projects. +OpenResourceAction_openOneNestedBelowSelection = The selected projects contain 1 closed nested project. +OpenResourceAction_openNestedBelowSelection = The selected projects contain {0} closed nested projects. +OpenResourceAction_open = &Open CloseResourceAction_text = Clo&se Project CloseResourceAction_text_plural = Clo&se Projects @@ -294,6 +300,14 @@ CloseResourceAction_title = Close Problems CloseResourceAction_problemMessage = Problems occurred closing the selected resources. CloseResourceAction_operationMessage = Closing project... CloseResourceAction_operationMessage_plural = Closing projects... +CloseResourceAction_promptTitle = Close Project +CloseResourceAction_closeOneNestedBelowProject = ''{0}'' contains 1 open nested project. +CloseResourceAction_closeNestedBelowProject = ''{1}'' contains {0} open nested projects. +CloseResourceAction_closeOneNestedBelowSelection = The selected projects contain 1 open nested project. +CloseResourceAction_closeNestedBelowSelection = The selected projects contain {0} open nested projects. +CloseResourceAction_close = &Close +RelatedProjectsDialog_includeNested = &Include nested projects +RelatedProjectsDialog_includeReferenced = Include &referenced projects CloseUnrelatedProjectsAction_text = Close &Unrelated Project CloseUnrelatedProjectsAction_text_plural = Close &Unrelated Projects @@ -539,6 +553,8 @@ IDEWorkspacePreference_defaultLineDelimProj=Inh&erited from container ({0}) IDEWorkspacePreference_otherLineDelim= Ot&her: IDEWorkspacePreference_relatedLink = See {0} for workspace startup and shutdown preferences. IDEWorkspacePreference_openReferencedProjects = Open referenced projects when a project is opened: +IDEWorkspacePreference_openNestedProjects = Open nested projects when a project is opened: +IDEWorkspacePreference_closeNestedProjects = Close nested projects when a project is closed: IDEWorkspacePreference_closeUnrelatedProjectsToolTip = Close unrelated projects without prompt IDEWorkspacePreference_windowTitleGroupText=Window title IDEWorkspacePreference_showLocationInWindowTitle=Show &full workspace path: diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/CloseResourceActionTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/CloseResourceActionTest.java new file mode 100644 index 00000000000..a0621809bcb --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/CloseResourceActionTest.java @@ -0,0 +1,210 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel 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.ui.tests.internal; + +import static org.eclipse.ui.tests.harness.util.UITestUtil.waitForJobs; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.actions.CloseResourceAction; +import org.eclipse.ui.internal.ide.IDEInternalPreferences; +import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; +import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Closing a project whose location contains further open projects. + */ +public class CloseResourceActionTest extends ResourceActionTest { + + private IProject parent; + private IProject nested; + private IPreferenceStore store; + private String oldPreference; + + @Before + public void createProjects() throws CoreException { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + parent = workspace.getRoot().getProject("CloseResourceActionTest_parent"); + parent.create(null); + parent.open(null); + nested = workspace.getRoot().getProject("CloseResourceActionTest_nested"); + IProjectDescription description = workspace.newProjectDescription(nested.getName()); + description.setLocation(parent.getLocation().append(nested.getName())); + nested.create(description, null); + nested.open(null); + store = IDEWorkbenchPlugin.getDefault().getPreferenceStore(); + oldPreference = store.getString(IDEInternalPreferences.CLOSE_NESTED_PROJECTS); + } + + @After + public void deleteProjects() throws CoreException { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, oldPreference); + nested.delete(true, null); + parent.delete(true, null); + waitForJobs(0, 30_000); + } + + @Test + public void testClosesOnlySelectedProjectByPreference() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_NEVER); + close(parent); + assertFalse(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + @Test + public void testClosesNestedProjectsByPreference() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_ALWAYS); + close(parent); + assertFalse(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testDoesNotPromptWhenNestedProjectIsSelected() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.CloseResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL); + close(parent, nested); + assertFalse(answered[0]); + assertFalse(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testDoesNotPromptWithoutNestedProjects() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.CloseResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL); + close(nested); + assertFalse(answered[0]); + assertTrue(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testClosesOnlySelectedProjectWhenNotIncluded() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.CloseResourceAction_promptTitle, + IDEWorkbenchMessages.CloseResourceAction_close); + close(parent); + assertTrue(answered[0]); + assertFalse(parent.isOpen()); + assertTrue(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_PROMPT, store.getString(IDEInternalPreferences.CLOSE_NESTED_PROJECTS)); + } + + @Test + public void testClosesNestedProjectsWhenIncluded() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.CloseResourceAction_promptTitle, + IDEWorkbenchMessages.CloseResourceAction_close, IDEWorkbenchMessages.RelatedProjectsDialog_includeNested); + close(parent); + assertTrue(answered[0]); + assertFalse(parent.isOpen()); + assertFalse(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_PROMPT, store.getString(IDEInternalPreferences.CLOSE_NESTED_PROJECTS)); + } + + @Test + public void testRemembersToIncludeNestedProjects() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.CloseResourceAction_promptTitle, + IDEWorkbenchMessages.CloseResourceAction_close, IDEWorkbenchMessages.RelatedProjectsDialog_includeNested, + JFaceResources.getString("MessageDialogWithToggle.defaultToggleMessage")); + close(parent); + assertTrue(answered[0]); + assertFalse(parent.isOpen()); + assertFalse(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_ALWAYS, store.getString(IDEInternalPreferences.CLOSE_NESTED_PROJECTS)); + } + + @Test + public void testRemembersToSkipNestedProjects() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.CloseResourceAction_promptTitle, + IDEWorkbenchMessages.CloseResourceAction_close, + JFaceResources.getString("MessageDialogWithToggle.defaultToggleMessage")); + close(parent); + assertTrue(answered[0]); + assertFalse(parent.isOpen()); + assertTrue(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_NEVER, store.getString(IDEInternalPreferences.CLOSE_NESTED_PROJECTS)); + } + + @Test + public void testCancelChangesNothing() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.CloseResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL, IDEWorkbenchMessages.RelatedProjectsDialog_includeNested, + JFaceResources.getString("MessageDialogWithToggle.defaultToggleMessage")); + close(parent); + assertTrue(answered[0]); + assertTrue(parent.isOpen()); + assertTrue(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_PROMPT, store.getString(IDEInternalPreferences.CLOSE_NESTED_PROJECTS)); + } + + @Test + public void testIncludesNestedProjectsByDefault() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = pressButton(IDEWorkbenchMessages.CloseResourceAction_promptTitle, + IDEWorkbenchMessages.CloseResourceAction_close); + close(parent); + assertTrue(answered[0]); + assertFalse(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testIgnoresNestedProjectsWhenTurnedOff() { + store.setValue(IDEInternalPreferences.CLOSE_NESTED_PROJECTS, IDEInternalPreferences.PSPM_ALWAYS); + boolean[] answered = answerDialog(IDEWorkbenchMessages.CloseResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL); + close(false, parent); + assertFalse(answered[0]); + assertFalse(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + private static void close(IProject... projects) { + close(true, projects); + } + + private static void close(boolean promptForNestedProjects, IProject... projects) { + Shell activeShell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + CloseResourceAction action = new CloseResourceAction(() -> activeShell); + action.setPromptForNestedProjects(promptForNestedProjects); + action.selectionChanged(new StructuredSelection(projects)); + assertTrue(action.isEnabled()); + action.run(); + processUIEvents(); + waitForJobs(0, 30_000); + } +} diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java index ff24811f308..1cf77ef6180 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/InternalTestSuite.java @@ -67,6 +67,8 @@ SaveablesListTest.class, Bug540297WorkbenchPageFindViewTest.class, Bug549139Test.class, + CloseResourceActionTest.class, + OpenResourceActionTest.class, LargeFileLimitsPreferenceHandlerTest.class, WorkbookEditorsHandlerTest.class, ScopeAreaTest.class, diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/OpenResourceActionTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/OpenResourceActionTest.java new file mode 100644 index 00000000000..3fe9c0075c5 --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/OpenResourceActionTest.java @@ -0,0 +1,307 @@ +/******************************************************************************* + * Copyright (c) 2026 Lars Vogel 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.ui.tests.internal; + +import static org.eclipse.ui.tests.harness.util.UITestUtil.waitForJobs; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.eclipse.core.resources.IProject; +import org.eclipse.core.resources.IProjectDescription; +import org.eclipse.core.resources.IWorkspace; +import org.eclipse.core.resources.ResourcesPlugin; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.jface.dialogs.IDialogConstants; +import org.eclipse.jface.preference.IPreferenceStore; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.viewers.StructuredSelection; +import org.eclipse.swt.widgets.Shell; +import org.eclipse.ui.PlatformUI; +import org.eclipse.ui.actions.OpenResourceAction; +import org.eclipse.ui.internal.ide.IDEInternalPreferences; +import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; +import org.eclipse.ui.internal.ide.IDEWorkbenchPlugin; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +/** + * Opening a project whose location contains further closed projects. + */ +public class OpenResourceActionTest extends ResourceActionTest { + + private IProject parent; + private IProject nested; + private IProject referenced; + private IPreferenceStore store; + private String oldPreference; + private String oldReferencedPreference; + + @Before + public void createProjects() throws CoreException { + IWorkspace workspace = ResourcesPlugin.getWorkspace(); + parent = workspace.getRoot().getProject("OpenResourceActionTest_parent"); + parent.create(null); + parent.open(null); + nested = workspace.getRoot().getProject("OpenResourceActionTest_nested"); + IProjectDescription description = workspace.newProjectDescription(nested.getName()); + description.setLocation(parent.getLocation().append(nested.getName())); + nested.create(description, null); + nested.open(null); + nested.close(null); + referenced = workspace.getRoot().getProject("OpenResourceActionTest_referenced"); + referenced.create(null); + referenced.open(null); + referenced.close(null); + IProjectDescription parentDescription = parent.getDescription(); + parentDescription.setReferencedProjects(new IProject[] { referenced }); + parent.setDescription(parentDescription, null); + parent.close(null); + store = IDEWorkbenchPlugin.getDefault().getPreferenceStore(); + oldPreference = store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS); + oldReferencedPreference = store.getString(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS); + // the nested tests ask only about nested projects + store.setValue(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_NEVER); + } + + @After + public void deleteProjects() throws CoreException { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, oldPreference); + store.setValue(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, oldReferencedPreference); + nested.delete(true, null); + referenced.delete(true, null); + parent.delete(true, null); + waitForJobs(0, 30_000); + } + + @Test + public void testOpensOnlySelectedProjectByPreference() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_NEVER); + open(parent); + assertTrue(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testOpensNestedProjectsByPreference() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_ALWAYS); + open(parent); + assertTrue(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + @Test + public void testDoesNotPromptWhenNestedProjectIsSelected() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL); + open(parent, nested); + assertFalse(answered[0]); + assertTrue(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + @Test + public void testDoesNotPromptWithoutNestedProjects() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL); + open(nested); + assertFalse(answered[0]); + assertFalse(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + @Test + public void testOpensOnlySelectedProjectWhenNotIncluded() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDEWorkbenchMessages.OpenResourceAction_open); + open(parent); + assertTrue(answered[0]); + assertTrue(parent.isOpen()); + assertFalse(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_PROMPT, store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS)); + } + + @Test + public void testOpensNestedProjectsWhenIncluded() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDEWorkbenchMessages.OpenResourceAction_open, IDEWorkbenchMessages.RelatedProjectsDialog_includeNested); + open(parent); + assertTrue(answered[0]); + assertTrue(parent.isOpen()); + assertTrue(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_PROMPT, store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS)); + } + + @Test + public void testRemembersToIncludeNestedProjects() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDEWorkbenchMessages.OpenResourceAction_open, IDEWorkbenchMessages.RelatedProjectsDialog_includeNested, + JFaceResources.getString("MessageDialogWithToggle.defaultToggleMessage")); + open(parent); + assertTrue(answered[0]); + assertTrue(parent.isOpen()); + assertTrue(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_ALWAYS, store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS)); + } + + @Test + public void testRemembersToSkipNestedProjects() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDEWorkbenchMessages.OpenResourceAction_open, + JFaceResources.getString("MessageDialogWithToggle.defaultToggleMessage")); + open(parent); + assertTrue(answered[0]); + assertTrue(parent.isOpen()); + assertFalse(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_NEVER, store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS)); + } + + @Test + public void testCancelChangesNothing() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL, IDEWorkbenchMessages.RelatedProjectsDialog_includeNested, + JFaceResources.getString("MessageDialogWithToggle.defaultToggleMessage")); + open(parent); + assertTrue(answered[0]); + assertFalse(parent.isOpen()); + assertFalse(nested.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_PROMPT, store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS)); + } + + @Test + public void testOpensReferencedProjectsByPreference() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_NEVER); + store.setValue(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_ALWAYS); + open(parent); + assertTrue(parent.isOpen()); + assertTrue(referenced.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testOpensReferencedProjectsWhenIncluded() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_NEVER); + store.setValue(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDEWorkbenchMessages.OpenResourceAction_open, IDEWorkbenchMessages.RelatedProjectsDialog_includeReferenced); + open(parent); + assertTrue(answered[0]); + assertTrue(parent.isOpen()); + assertTrue(referenced.isOpen()); + assertFalse(nested.isOpen()); + } + + @Test + public void testSkipsReferencedProjectsWhenNotIncluded() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_NEVER); + store.setValue(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDEWorkbenchMessages.OpenResourceAction_open); + open(parent); + assertTrue(answered[0]); + assertTrue(parent.isOpen()); + assertFalse(referenced.isOpen()); + } + + @Test + public void testAsksBothQuestionsAtOnce() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + store.setValue(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDEWorkbenchMessages.OpenResourceAction_open, IDEWorkbenchMessages.RelatedProjectsDialog_includeNested, + IDEWorkbenchMessages.RelatedProjectsDialog_includeReferenced, + JFaceResources.getString("MessageDialogWithToggle.defaultToggleMessage")); + open(parent); + assertTrue(answered[0]); + assertTrue(parent.isOpen()); + assertTrue(nested.isOpen()); + assertTrue(referenced.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_ALWAYS, store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS)); + assertEquals(IDEInternalPreferences.PSPM_ALWAYS, store.getString(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS)); + } + + @Test + public void testCancelWithBothQuestionsChangesNothing() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + store.setValue(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL, IDEWorkbenchMessages.RelatedProjectsDialog_includeNested, + IDEWorkbenchMessages.RelatedProjectsDialog_includeReferenced, + JFaceResources.getString("MessageDialogWithToggle.defaultToggleMessage")); + open(parent); + assertTrue(answered[0]); + assertFalse(parent.isOpen()); + assertFalse(nested.isOpen()); + assertFalse(referenced.isOpen()); + assertEquals(IDEInternalPreferences.PSPM_PROMPT, store.getString(IDEInternalPreferences.OPEN_NESTED_PROJECTS)); + assertEquals(IDEInternalPreferences.PSPM_PROMPT, store.getString(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS)); + } + + @Test + public void testDoesNotAskWhenReferencedProjectIsOpen() throws CoreException { + referenced.open(null); + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_NEVER); + store.setValue(IDEInternalPreferences.OPEN_REQUIRED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL); + open(parent); + assertFalse(answered[0]); + assertTrue(parent.isOpen()); + } + + @Test + public void testIncludesNestedProjectsByDefault() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_PROMPT); + boolean[] answered = pressButton(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDEWorkbenchMessages.OpenResourceAction_open); + open(parent); + assertTrue(answered[0]); + assertTrue(parent.isOpen()); + assertTrue(nested.isOpen()); + } + + @Test + public void testIgnoresNestedProjectsWhenTurnedOff() { + store.setValue(IDEInternalPreferences.OPEN_NESTED_PROJECTS, IDEInternalPreferences.PSPM_ALWAYS); + boolean[] answered = answerDialog(IDEWorkbenchMessages.OpenResourceAction_promptTitle, + IDialogConstants.CANCEL_LABEL); + open(false, parent); + assertFalse(answered[0]); + assertTrue(parent.isOpen()); + assertFalse(nested.isOpen()); + } + + private static void open(IProject... projects) { + open(true, projects); + } + + private static void open(boolean promptForNestedProjects, IProject... projects) { + Shell activeShell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + OpenResourceAction action = new OpenResourceAction(() -> activeShell); + action.setPromptForNestedProjects(promptForNestedProjects); + action.selectionChanged(new StructuredSelection(projects)); + assertTrue(action.isEnabled()); + action.run(); + processUIEvents(); + waitForJobs(0, 30_000); + } +} diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/ResourceActionTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/ResourceActionTest.java index 33145085d82..e7f7bb2c876 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/ResourceActionTest.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/ResourceActionTest.java @@ -1,8 +1,18 @@ package org.eclipse.ui.tests.internal; +import static org.junit.Assert.assertNotNull; + +import java.util.ArrayList; +import java.util.List; + import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.window.IShellProvider; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Button; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Shell; import org.eclipse.ui.actions.DeleteResourceAction; import org.eclipse.ui.internal.ide.IDEWorkbenchMessages; import org.eclipse.ui.internal.operations.AdvancedValidationUserApprover; @@ -22,6 +32,7 @@ public void setUp() throws Exception { @After public void tearDown() throws Exception { AdvancedValidationUserApprover.AUTOMATED_MODE = false; + pollers.clear(); } protected static void joinDeleteResourceActionJobs() { @@ -46,6 +57,99 @@ private static void joinJob(String deleteActionJobName) { } } + private static final List pollers = new ArrayList<>(); + + /** + * Answers the next modal dialog with the given title from the event loop it + * runs: checks exactly the check boxes with the given labels and presses the + * button with the given label. The returned flag reports whether the dialog + * was seen. Polling stops at the end of the test. + */ + protected static boolean[] answerDialog(String title, String button, String... checkBoxes) { + return answerDialog(title, button, List.of(checkBoxes)); + } + + /** + * Presses the button with the given label in the next modal dialog with the + * given title, leaving its check boxes as they are. + */ + protected static boolean[] pressButton(String title, String button) { + return answerDialog(title, button, (List) null); + } + + private static boolean[] answerDialog(String title, String button, List checkBoxes) { + boolean[] answered = { false }; + Display display = Display.getCurrent(); + Runnable poller = new Runnable() { + @Override + public void run() { + if (!pollers.contains(this)) { + return; + } + Shell shell = findShell(display, title); + if (shell == null) { + display.timerExec(50, this); + return; + } + pollers.remove(this); + if (checkBoxes != null) { + List checked = new ArrayList<>(); + for (String checkBox : checkBoxes) { + assertNotNull(checkBox, findButton(shell, checkBox)); + checked.add(checkBox.replace("&", "")); + } + for (Button check : findCheckBoxes(shell, new ArrayList<>())) { + check.setSelection(checked.contains(check.getText().replace("&", ""))); + check.notifyListeners(SWT.Selection, null); + } + } + Button push = findButton(shell, button); + assertNotNull(button, push); + answered[0] = true; + push.notifyListeners(SWT.Selection, null); + } + }; + pollers.add(poller); + display.timerExec(50, poller); + return answered; + } + + private static Shell findShell(Display display, String title) { + for (Shell shell : display.getShells()) { + if (title.equals(shell.getText()) && shell.isVisible()) { + return shell; + } + } + return null; + } + + private static List