From 0a3c1fd31e73bc5bb8bd6a94f86922d30456b4f2 Mon Sep 17 00:00:00 2001 From: Feilim Breatnach Date: Thu, 10 Oct 2024 16:04:25 +0100 Subject: [PATCH] Modify the 'Close Active Editors' (plural) handler to add support for parts which represent an Editor and are contributed via eg. PartDescriptors in a Model Fragment. Associated with Issue#2176. Include JUnit test. --- .../eclipse/ui/internal/CloseAllHandler.java | 70 +++++ .../eclipse/ui/tests/api/ApiTestSuite.java | 4 +- .../ui/tests/e4/CloseAllHandlerTest.java | 282 ++++++++++++++++++ .../org/eclipse/ui/tests/e4/DummyEditor.java | 17 ++ 4 files changed, 372 insertions(+), 1 deletion(-) create mode 100644 tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/e4/CloseAllHandlerTest.java create mode 100644 tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/e4/DummyEditor.java diff --git a/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/CloseAllHandler.java b/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/CloseAllHandler.java index 7a431c14292..45d8d0b5a95 100644 --- a/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/CloseAllHandler.java +++ b/bundles/org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/CloseAllHandler.java @@ -14,18 +14,32 @@ package org.eclipse.ui.internal; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; import org.eclipse.core.commands.ExecutionEvent; import org.eclipse.core.commands.ExecutionException; import org.eclipse.core.expressions.EvaluationResult; import org.eclipse.core.expressions.Expression; import org.eclipse.core.expressions.ExpressionInfo; import org.eclipse.core.expressions.IEvaluationContext; +import org.eclipse.e4.ui.model.application.MApplication; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.workbench.IWorkbench; +import org.eclipse.e4.ui.workbench.modeling.EModelService; +import org.eclipse.e4.ui.workbench.modeling.EPartService; import org.eclipse.ui.IEditorReference; import org.eclipse.ui.ISources; import org.eclipse.ui.IWorkbenchPage; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindow; import org.eclipse.ui.handlers.HandlerUtil; +import org.eclipse.ui.internal.e4.compatibility.CompatibilityEditor; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; /** * Closes all active editors @@ -48,6 +62,25 @@ public Object execute(ExecutionEvent event) throws ExecutionException { IWorkbenchPage page = window.getActivePage(); if (page != null) { page.closeAllEditors(true); + + // close parts representing editors which were contributed via + // eg. model fragment(s) + Collection partsTaggedAsEditor = getContributedPartsTaggedAsEditor(); + if (!partsTaggedAsEditor.isEmpty()) { + MApplication application = getApplicationModel(); + EPartService partService = application.getContext().get(EPartService.class); + if (partService != null) { + for (MPart part : partsTaggedAsEditor) { + if (partService.savePart(part, true)) { + partService.hidePart(part); + } + } + // ensure the EnabledWhenExpression evaluation is performed + // otherwise the 'Close All Editors' will still appear enabled until + // the user clicks/selects a different part + getEvaluationService().requestEvaluation(ISources.ACTIVE_PART_NAME); + } + } } return null; @@ -69,6 +102,12 @@ public EvaluationResult evaluate(IEvaluationContext context) { if (refArray != null && refArray.length > 0) { return EvaluationResult.TRUE; } + + // determine if we have any part contributions via model fragment + // which were tagged as being an 'Editor' (and which are to be rendered) + if (!getContributedPartsTaggedAsEditor().isEmpty()) { + return EvaluationResult.TRUE; + } } } return EvaluationResult.FALSE; @@ -83,4 +122,35 @@ public void collectExpressionInfo(ExpressionInfo info) { } return enabledWhen; } + + /** + * Collects part contributions from the application model which are not + * associated with compatibility layer editors, and are instead parts + * contributed via eg. model fragment, and which were tagged as representing an + * Editor, via the {@link Workbench#EDITOR_TAG} tag. + * + * @return a collection of (closable) part contributions from the application + * model, tagged as 'Editor' and not containing the parts associated + * with compatibility layer editors. Returns an empty collection if none + * are found + */ + private Collection getContributedPartsTaggedAsEditor() { + MApplication application = getApplicationModel(); + EModelService modelService = application.getContext().get(EModelService.class); + + List partsTaggedAsEditor = modelService != null + ? modelService.findElements(application, null, MPart.class, Arrays.asList(Workbench.EDITOR_TAG)) + : Collections.emptyList(); + + // remove parts which we wish to ignore: compatibility layer editors, + // non-closable parts, non-rendered parts + return partsTaggedAsEditor.stream().filter(p -> !CompatibilityEditor.MODEL_ELEMENT_ID.equals(p.getElementId()) + && p.isCloseable() && p.isToBeRendered()).collect(Collectors.toSet()); + } + + private MApplication getApplicationModel() { + BundleContext bundleContext = FrameworkUtil.getBundle(IWorkbench.class).getBundleContext(); + ServiceReference reference = bundleContext.getServiceReference(IWorkbench.class); + return bundleContext.getService(reference).getApplication(); + } } diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/api/ApiTestSuite.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/api/ApiTestSuite.java index abc15b31936..661ea136d4b 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/api/ApiTestSuite.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/api/ApiTestSuite.java @@ -21,6 +21,7 @@ import org.eclipse.ui.tests.api.workbenchpart.OverriddenTitleTest; import org.eclipse.ui.tests.api.workbenchpart.RawIViewPartTest; import org.eclipse.ui.tests.api.workbenchpart.ViewPartTitleTest; +import org.eclipse.ui.tests.e4.CloseAllHandlerTest; import org.eclipse.ui.tests.ide.api.FileEditorInputTest; import org.eclipse.ui.tests.ide.api.IDETest; import org.eclipse.ui.tests.ide.api.IDETest2; @@ -83,7 +84,8 @@ SaveablesListTest.class, PerspectiveExtensionReaderTest.class, ModeledPageLayoutTest.class, - WorkbenchPluginTest.class + WorkbenchPluginTest.class, + CloseAllHandlerTest.class }) public class ApiTestSuite { diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/e4/CloseAllHandlerTest.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/e4/CloseAllHandlerTest.java new file mode 100644 index 00000000000..194726c5d62 --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/e4/CloseAllHandlerTest.java @@ -0,0 +1,282 @@ +/******************************************************************************* +* Copyright (c) 2024 Feilim Breatnach 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: Feilim Breatnach, Pilz Ireland - PR #2360 +*******************************************************************************/ + +package org.eclipse.ui.tests.e4; + +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.eclipse.core.commands.Command; +import org.eclipse.core.commands.ParameterizedCommand; +import org.eclipse.core.resources.IFile; +import org.eclipse.core.resources.IStorage; +import org.eclipse.e4.core.commands.ECommandService; +import org.eclipse.e4.core.commands.EHandlerService; +import org.eclipse.e4.core.contexts.IEclipseContext; +import org.eclipse.e4.ui.model.application.MApplication; +import org.eclipse.e4.ui.model.application.descriptor.basic.MPartDescriptor; +import org.eclipse.e4.ui.model.application.ui.advanced.MArea; +import org.eclipse.e4.ui.model.application.ui.basic.MPart; +import org.eclipse.e4.ui.model.application.ui.basic.MPartSashContainer; +import org.eclipse.e4.ui.model.application.ui.basic.MPartSashContainerElement; +import org.eclipse.e4.ui.model.application.ui.basic.MPartStack; +import org.eclipse.e4.ui.workbench.IWorkbench; +import org.eclipse.e4.ui.workbench.modeling.EModelService; +import org.eclipse.e4.ui.workbench.modeling.EPartService; +import org.eclipse.e4.ui.workbench.modeling.EPartService.PartState; +import org.eclipse.jface.resource.ImageDescriptor; +import org.eclipse.ui.IEditorPart; +import org.eclipse.ui.IFileEditorInput; +import org.eclipse.ui.IPageLayout; +import org.eclipse.ui.IPersistableElement; +import org.eclipse.ui.ISources; +import org.eclipse.ui.IWorkbenchWindow; +import org.eclipse.ui.PartInitException; +import org.eclipse.ui.internal.CloseAllHandler; +import org.eclipse.ui.internal.Workbench; +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.osgi.framework.BundleContext; +import org.osgi.framework.FrameworkUtil; +import org.osgi.framework.ServiceReference; + +/** + * Tests the enabled when and execution logic within the + * {@link CloseAllHandler}. + */ +public class CloseAllHandlerTest { + + private IEclipseContext applicationContext; + private MApplication application; + private EModelService modelService; + private EPartService partService; + + private static final String TEST_COMPATIBILITY_LAYER_EDITOR_ID = "org.eclipse.ui.tests.TitleTestEditor"; //$NON-NLS-1$ + private static final String CLOSE_ALL_EDITORS_COMMAND_ID = "org.eclipse.ui.file.closeAll"; //$NON-NLS-1$ + private static final String DUMMY_E4_PART_ID = "e4_dummy_part_editor"; //$NON-NLS-1$ + + @Before + public void setUp() throws Exception { + application = getApplicationModel(); + applicationContext = application.getContext(); + modelService = applicationContext.get(EModelService.class); + partService = application.getContext().get(EPartService.class); + } + + private MApplication getApplicationModel() { + BundleContext bundleContext = FrameworkUtil.getBundle(IWorkbench.class).getBundleContext(); + ServiceReference reference = bundleContext.getServiceReference(IWorkbench.class); + return bundleContext.getService(reference).getApplication(); + } + + /** + * Tests the enabled when and execution logic within the + * {@link CloseAllHandler}. + * + * Scenario 1: E4 style part contribution which is tagged as representing an + * 'editor' is closed via the handler (and the enablement of handler is + * checked). + * + * Scenario 2: compatibility layer type editor is closed via the handler (and + * the enablement of handler is checked). + * + * Scenario 3: a mix of an open compatibility layer type editor *and* an E4 + * style part contribution which is tagged as representing an 'editor' are both + * closed via the handler (and the enablement of handler is checked). + */ + @Test + public void testCloseMixedEditorTypes() { + EHandlerService handlerService = application.getContext().get(EHandlerService.class); + ECommandService commandService = application.getContext().get(ECommandService.class); + + Command closeAllCommand = commandService.getCommand(CLOSE_ALL_EDITORS_COMMAND_ID); + final ParameterizedCommand parameterizedCommand = ParameterizedCommand.generateCommand(closeAllCommand, + Collections.EMPTY_MAP); + + // verify the close all editors handler enabledment is false (no editors are + // open yet!) + boolean canExecute = handlerService.canExecute(parameterizedCommand); + Assert.assertFalse(canExecute); + + // scenario 1: e4 part descriptor contribution + MPartDescriptor partDescriptor = createDummyPartDescriptor(); + application.getDescriptors().add(partDescriptor); + + // open our e4 part which represents an editor + MPart dummyPart = createAndOpenE4Part(partDescriptor); + + // verify the close all handler is enabled now (since dummy editor has been + // opened) + canExecute = handlerService.canExecute(parameterizedCommand); + Assert.assertTrue(canExecute); + + // close all editors (dummy editor should close!) + dummyPart = partService.findPart(DUMMY_E4_PART_ID); + Assert.assertNotNull(dummyPart); + handlerService.executeHandler(parameterizedCommand); + dummyPart = partService.findPart(DUMMY_E4_PART_ID); + Assert.assertNull(dummyPart); + + // verify the close all handler is *not* enabled now (since dummy editor has + // been closed) + canExecute = handlerService.canExecute(parameterizedCommand); + Assert.assertFalse(canExecute); + + // scenario 2: open a compatibility layer editor + IFileEditorInput input = new DummyFileEditorInput(); + Object activeWindow = applicationContext.getActive(ISources.ACTIVE_WORKBENCH_WINDOW_NAME); + Assert.assertTrue("Active workbench window not found.", activeWindow instanceof IWorkbenchWindow); + IWorkbenchWindow window = (IWorkbenchWindow) activeWindow; + try { + window.getActivePage().openEditor(input, TEST_COMPATIBILITY_LAYER_EDITOR_ID); + } catch (PartInitException e) { + Assert.fail("Test Compatibility Editor could not be opened. Further testing cannot complete."); + } + + // verify the close all handler is enabled now (since a dummy compatibility + // layer editor has been opened) + canExecute = handlerService.canExecute(parameterizedCommand); + Assert.assertTrue(canExecute); + + IEditorPart compatEditor = window.getActivePage().findEditor(input); + Assert.assertNotNull(compatEditor); + handlerService.executeHandler(parameterizedCommand); + compatEditor = window.getActivePage().findEditor(input); + Assert.assertNull(compatEditor); + + // verify the close all handler is *not* enabled now (since compatibility layer + // editor has been closed) + canExecute = handlerService.canExecute(parameterizedCommand); + Assert.assertFalse(canExecute); + + // scenario 3: + // finally: re-open both the compatibility layer editor *and* the dummy e4 part + // which represents an editor, and verify they are *both* closed when we invoked + // the close all editors handler + dummyPart = createAndOpenE4Part(partDescriptor); + try { + window.getActivePage().openEditor(input, TEST_COMPATIBILITY_LAYER_EDITOR_ID); + } catch (PartInitException e) { + Assert.fail("Test Compatibility Editor could not be opened. Further testing cannot complete."); + } + compatEditor = window.getActivePage().findEditor(input); + Assert.assertNotNull(compatEditor); + dummyPart = partService.findPart(DUMMY_E4_PART_ID); + Assert.assertNotNull(dummyPart); + + canExecute = handlerService.canExecute(parameterizedCommand); + Assert.assertTrue(canExecute); + + // close all editors + handlerService.executeHandler(parameterizedCommand); + canExecute = handlerService.canExecute(parameterizedCommand); + Assert.assertFalse(canExecute); + + // verify they are all closed + compatEditor = window.getActivePage().findEditor(input); + Assert.assertNull(compatEditor); + dummyPart = partService.findPart(DUMMY_E4_PART_ID); + Assert.assertNull(dummyPart); + } + + private MPart createAndOpenE4Part(MPartDescriptor partDescriptor) { + Optional primaryPartStack = findPrimaryConfiguationAreaPartStack(application, modelService); + + if (primaryPartStack.isEmpty()) { + Assert.fail("Test cannot proceed as the primary part stack could not be found in the application."); + } + + MPart dummyPart = partService.createPart(partDescriptor.getElementId()); + primaryPartStack.get().getChildren().add(dummyPart); + partService.showPart(dummyPart.getElementId(), PartState.ACTIVATE); + partService.bringToTop(dummyPart); + + return dummyPart; + } + + private MPartDescriptor createDummyPartDescriptor() { + MPartDescriptor partDescriptor = modelService.createModelElement(MPartDescriptor.class); + partDescriptor.setAllowMultiple(true); + partDescriptor.setElementId(DUMMY_E4_PART_ID); + partDescriptor.setCloseable(true); + partDescriptor.setLabel(DUMMY_E4_PART_ID); + partDescriptor.getTags().add(Workbench.EDITOR_TAG); + partDescriptor.getTags().add(EPartService.REMOVE_ON_HIDE_TAG); + partDescriptor.setContributionURI("bundleclass://org.eclipse.ui.tests/org.eclipse.ui.tests.e4.DummyEditor"); + + return partDescriptor; + } + + private Optional findPrimaryConfiguationAreaPartStack(MApplication application, + EModelService modelService) { + List areaCandidates = modelService.findElements(application, + IPageLayout.ID_EDITOR_AREA, MArea.class, null, + EModelService.IN_SHARED_ELEMENTS); + if (areaCandidates.size() == 1) { + MArea primaryArea = areaCandidates.get(0); + for (MPartSashContainerElement element : primaryArea.getChildren()) { + if (element instanceof MPartStack) { + return Optional.of((MPartStack) element); + } else if (element instanceof MPartSashContainer) { + return ((MPartSashContainer) element).getChildren().stream().filter(c -> c instanceof MPartStack) + .map(c -> (MPartStack) c).findFirst(); + } + } + } + + return Optional.empty(); + } + + private class DummyFileEditorInput implements IFileEditorInput { + @Override + public boolean exists() { + return true; + } + + @Override + public ImageDescriptor getImageDescriptor() { + return null; + } + + @Override + public String getName() { + return "MyInputFile"; + } + + @Override + public IPersistableElement getPersistable() { + return null; + } + + @Override + public String getToolTipText() { + return "My Input File"; + } + + @Override + public T getAdapter(Class adapter) { + return null; + } + + @Override + public IFile getFile() { + return null; + } + + @Override + public IStorage getStorage() { + return null; + } + } +} \ No newline at end of file diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/e4/DummyEditor.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/e4/DummyEditor.java new file mode 100644 index 00000000000..63c7db7cff1 --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/e4/DummyEditor.java @@ -0,0 +1,17 @@ +/******************************************************************************* +* Copyright (c) 2024 Feilim Breatnach 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: Feilim Breatnach, Pilz Ireland - PR #2360 +*******************************************************************************/ + +package org.eclipse.ui.tests.e4; + +public class DummyEditor { + +}