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..594024646fe 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,9 @@ 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; + /** * Creates a new action. * @@ -179,6 +186,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 +194,13 @@ public void run() { return; } + projects = promptForProjectsToClose(projects); + if (projects == null) { + // the user cancelled the operation + return; + } + resourcesToClose = projects; + final IResource[] projectArray = projects.toArray(new IResource[projects.size()]); if (!IDE.saveAllEditors(projectArray, true)) { @@ -219,6 +234,77 @@ 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); + } + + /** + * @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 true; + } + /** * 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..b881dbd3387 --- /dev/null +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/NestedProjects.java @@ -0,0 +1,69 @@ +/******************************************************************************* + * 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.List; + +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(); + } + List nestedProjects = new ArrayList<>(); + for (IProject project : ResourcesPlugin.getWorkspace().getRoot().getProjects()) { + if (project.isOpen() != open || projects.contains(project)) { + continue; + } + IPath location = project.getLocation(); + if (location == null) { + continue; + } + for (IPath selected : locations) { + if (!selected.equals(location) && selected.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..11f133d28ce 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,6 +12,7 @@ * 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; @@ -19,28 +20,26 @@ import java.util.List; 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; @@ -133,59 +132,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 +160,68 @@ public void resourceChanged(IResourceChangeEvent event) { @Override public void run() { - try { - runOpenWithReferences(); - } catch (OperationCanceledException e) { - //just return when canceled + List projects = getActionResources(); + List nestedProjects = NestedProjects.below(projects, false); + 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); + } + + 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) { + List candidates = new ArrayList<>(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 +233,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 void runOpenWithReferences() { - final List resources = new ArrayList<>(getActionResources()); + 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(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 +271,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..7cd2af547de --- /dev/null +++ b/bundles/org.eclipse.ui.ide/extensions/org/eclipse/ui/actions/RelatedProjectsDialog.java @@ -0,0 +1,120 @@ +/******************************************************************************* + * 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); + } + 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..4cdd557ac1d 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; @@ -144,6 +148,8 @@ protected Control createContents(Composite parent) { Composite comboParent = new Composite(composite, SWT.NONE); comboParent.setLayout(new GridLayout(2, false)); createOpenPrefControls(comboParent); + createOpenNestedPrefControls(comboParent); + createCloseNestedPrefControls(comboParent); createMissingNaturePref(comboParent); createMissingEncodingPref(comboParent); @@ -236,6 +242,38 @@ private void createOpenPrefControls(Composite parent) { openReferencesEditor.load(); } + /** + * Creates controls for the preference to open nested projects. + */ + private void createOpenNestedPrefControls(Composite parent) { + String name = IDEInternalPreferences.OPEN_NESTED_PROJECTS; + String label = IDEWorkbenchMessages.IDEWorkspacePreference_openNestedProjects; + String[][] namesAndValues = { + { Action.removeMnemonics(IDEWorkbenchMessages.Always), IDEInternalPreferences.PSPM_ALWAYS }, + { Action.removeMnemonics(IDEWorkbenchMessages.Never), IDEInternalPreferences.PSPM_NEVER }, + { Action.removeMnemonics(IDEWorkbenchMessages.Prompt), IDEInternalPreferences.PSPM_PROMPT } }; + openNestedEditor = new ComboFieldEditorInGrid(name, label, namesAndValues, parent); + openNestedEditor.setPreferenceStore(getIDEPreferenceStore()); + openNestedEditor.setPage(this); + openNestedEditor.load(); + } + + /** + * Creates controls for the preference to close nested projects. + */ + private void createCloseNestedPrefControls(Composite parent) { + String name = IDEInternalPreferences.CLOSE_NESTED_PROJECTS; + String label = IDEWorkbenchMessages.IDEWorkspacePreference_closeNestedProjects; + String[][] namesAndValues = { + { Action.removeMnemonics(IDEWorkbenchMessages.Always), IDEInternalPreferences.PSPM_ALWAYS }, + { Action.removeMnemonics(IDEWorkbenchMessages.Never), IDEInternalPreferences.PSPM_NEVER }, + { Action.removeMnemonics(IDEWorkbenchMessages.Prompt), IDEInternalPreferences.PSPM_PROMPT } }; + closeNestedEditor = new ComboFieldEditorInGrid(name, label, namesAndValues, parent); + closeNestedEditor.setPreferenceStore(getIDEPreferenceStore()); + closeNestedEditor.setPage(this); + closeNestedEditor.load(); + } + /** * Creates controls for the preference to close unrelated projects. * @param parent The parent control @@ -562,6 +600,8 @@ protected void performDefaults() { encodingEditor.loadDefault(); lineSeparatorEditor.loadDefault(); openReferencesEditor.loadDefault(); + openNestedEditor.loadDefault(); + closeNestedEditor.loadDefault(); missingNatureSeverityCombo.loadDefault(); missingEncodingSeverityCombo.loadDefault(); @@ -628,6 +668,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..0dfd33dadee --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/CloseResourceActionTest.java @@ -0,0 +1,183 @@ +/******************************************************************************* + * 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)); + } + + private static void close(IProject... projects) { + Shell activeShell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + CloseResourceAction action = new CloseResourceAction(() -> activeShell); + 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..bd999ee49d0 --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/internal/OpenResourceActionTest.java @@ -0,0 +1,280 @@ +/******************************************************************************* + * 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()); + } + + private static void open(IProject... projects) { + Shell activeShell = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(); + OpenResourceAction action = new OpenResourceAction(() -> activeShell); + 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..c35494dcca6 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,71 @@ 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: sets 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) { + 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); + for (String checkBox : checkBoxes) { + Button check = findButton(shell, checkBox); + assertNotNull(checkBox, check); + check.setSelection(true); + 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 Button findButton(Composite parent, String label) { + String plain = label.replace("&", ""); + for (Control child : parent.getChildren()) { + if (child instanceof Button button && plain.equals(button.getText().replace("&", ""))) { + return button; + } + if (child instanceof Composite composite) { + Button found = findButton(composite, label); + if (found != null) { + return found; + } + } + } + return null; + } + /** * After an internal action, see if there are any outstanding SWT events. */