| /******************************************************************************* |
| * Copyright (c) 2000, 2015 IBM Corporation and others. |
| * All rights reserved. This program and the accompanying materials |
| * are made available under the terms of the Eclipse Public License v1.0 |
| * which accompanies this distribution, and is available at |
| * http://www.eclipse.org/legal/epl-v10.html |
| * |
| * Contributors: |
| * IBM Corporation - initial API and implementation |
| * Dan Rubel (dan_rubel@instantiations.com) - accessor to get menu id |
| * Lars Vogel <Lars.Vogel@vogella.com> - Bug 472654 |
| *******************************************************************************/ |
| package org.eclipse.ui.internal; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Set; |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.core.runtime.IExtensionDelta; |
| import org.eclipse.core.runtime.IExtensionPoint; |
| import org.eclipse.core.runtime.IRegistryChangeEvent; |
| import org.eclipse.core.runtime.IRegistryChangeListener; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.e4.core.contexts.IEclipseContext; |
| import org.eclipse.e4.ui.internal.workbench.ContributionsAnalyzer; |
| import org.eclipse.e4.ui.internal.workbench.OpaqueElementUtil; |
| import org.eclipse.e4.ui.internal.workbench.swt.AbstractPartRenderer; |
| import org.eclipse.e4.ui.internal.workbench.swt.MenuService; |
| import org.eclipse.e4.ui.model.application.ui.MElementContainer; |
| import org.eclipse.e4.ui.model.application.ui.MUIElement; |
| import org.eclipse.e4.ui.model.application.ui.basic.MPart; |
| import org.eclipse.e4.ui.model.application.ui.menu.MMenu; |
| import org.eclipse.e4.ui.model.application.ui.menu.MMenuElement; |
| import org.eclipse.e4.ui.model.application.ui.menu.MPopupMenu; |
| import org.eclipse.e4.ui.model.application.ui.menu.impl.MenuFactoryImpl; |
| import org.eclipse.e4.ui.workbench.renderers.swt.MenuManagerRenderer; |
| import org.eclipse.e4.ui.workbench.swt.factories.IRendererFactory; |
| import org.eclipse.jface.action.IContributionItem; |
| import org.eclipse.jface.action.IMenuListener2; |
| import org.eclipse.jface.action.IMenuManager; |
| import org.eclipse.jface.action.MenuManager; |
| import org.eclipse.jface.action.Separator; |
| import org.eclipse.jface.action.SubMenuManager; |
| import org.eclipse.jface.viewers.ISelection; |
| import org.eclipse.jface.viewers.ISelectionChangedListener; |
| import org.eclipse.jface.viewers.ISelectionProvider; |
| import org.eclipse.jface.viewers.StructuredSelection; |
| import org.eclipse.swt.widgets.Display; |
| import org.eclipse.ui.IEditorPart; |
| import org.eclipse.ui.IWorkbench; |
| import org.eclipse.ui.IWorkbenchPart; |
| import org.eclipse.ui.IWorkbenchPartSite; |
| import org.eclipse.ui.internal.registry.IWorkbenchRegistryConstants; |
| |
| /** |
| * This class extends a single popup menu |
| */ |
| public class PopupMenuExtender implements IMenuListener2, |
| IRegistryChangeListener { |
| |
| /** |
| * The bit in <code>bitSet</code> that stores whether the static actions |
| * have been read from the registry. |
| */ |
| private static final int STATIC_ACTION_READ = 1; |
| |
| /** |
| * The bit in <code>bitSet</code> that stores whether the editor input |
| * should be included for the sake of object contributions. |
| */ |
| private static final int INCLUDE_EDITOR_INPUT = 1 << 1; |
| |
| private final MenuManager menu; |
| |
| private SubMenuManager menuWrapper; |
| |
| private final ISelectionProvider selProvider; |
| |
| private final IWorkbenchPart part; |
| |
| private Map<String, ViewerActionBuilder> staticActionBuilders = null; |
| |
| /** |
| * The boolean properties maintained by this extender. A bit set is used to |
| * save memory. |
| */ |
| private int bitSet = 0; |
| |
| private ArrayList<PluginActionContributionItem> actionContributionCache = new ArrayList<>(); |
| private boolean cleanupNeeded = false; |
| |
| private MPart modelPart; |
| |
| /** |
| * The context that will be used to create the popup menu's context under. |
| */ |
| private IEclipseContext context; |
| |
| /** |
| * Construct a new menu extender. |
| * |
| * @param id |
| * the menu id |
| * @param menu |
| * the menu to extend |
| * @param prov |
| * the selection provider |
| * @param part |
| * the part to extend |
| * @param context |
| * the context to create the child popup menu context under |
| */ |
| public PopupMenuExtender(String id, MenuManager menu, ISelectionProvider prov, |
| IWorkbenchPart part, IEclipseContext context) { |
| this(id, menu, prov, part, context, true); |
| } |
| |
| /** |
| * Construct a new menu extender. |
| * |
| * @param id |
| * the menu id |
| * @param menu |
| * the menu to extend |
| * @param prov |
| * the selection provider |
| * @param part |
| * the part to extend |
| * @param context |
| * the context to create the child popup menu context under |
| * @param includeEditorInput |
| * Whether the editor input should be included when adding object |
| * contributions to this context menu. |
| */ |
| public PopupMenuExtender(final String id, final MenuManager menu, |
| final ISelectionProvider prov, final IWorkbenchPart part, IEclipseContext context, |
| final boolean includeEditorInput) { |
| super(); |
| this.menu = menu; |
| this.selProvider = prov; |
| this.part = part; |
| this.context = context; |
| this.modelPart = part.getSite().getService(MPart.class); |
| if (includeEditorInput) { |
| bitSet |= INCLUDE_EDITOR_INPUT; |
| } |
| menu.addMenuListener(this); |
| if (!menu.getRemoveAllWhenShown()) { |
| menuWrapper = new SubMenuManager(menu); |
| menuWrapper.setVisible(true); |
| } |
| createModelFor(id); |
| addMenuId(id); |
| |
| Platform.getExtensionRegistry().addRegistryChangeListener(this); |
| } |
| |
| private void createModelFor(String id) { |
| if (id == null) { |
| id = getClass().getName() + '.' + System.identityHashCode(this); |
| } |
| menuModel = null; |
| for (MMenu item : modelPart.getMenus()) { |
| if (id.equals(item.getElementId()) && item instanceof MPopupMenu |
| && item.getTags().contains("popup")) { //$NON-NLS-1$ |
| menuModel = (MPopupMenu) item; |
| break; |
| } |
| } |
| if (menuModel == null) { |
| menuModel = MenuFactoryImpl.eINSTANCE.createPopupMenu(); |
| menuModel.setElementId(id); |
| menuModel.getTags().add(ContributionsAnalyzer.MC_POPUP); |
| modelPart.getMenus().add(menuModel); |
| } |
| IRendererFactory factory = modelPart.getContext().get(IRendererFactory.class); |
| AbstractPartRenderer obj = factory.getRenderer(menuModel, null); |
| if (obj instanceof MenuManagerRenderer) { |
| ((MenuManagerRenderer) obj).linkModelToManager(menuModel, menu); |
| } |
| registerE4Support(); |
| cleanUpContributionCache(); |
| } |
| |
| private void registerE4Support() { |
| if (menuModel.getWidget() == null && menu.getMenu() != null) { |
| MenuService.registerMenu(menu.getMenu().getParent(), menuModel, context); |
| } |
| } |
| // getMenuId() added by Dan Rubel (dan_rubel@instantiations.com) |
| /** |
| * Return the menu identifiers for this extender. |
| * |
| * @return The set of all identifiers that represent this extender. |
| */ |
| public Set<String> getMenuIds() { |
| if (staticActionBuilders == null) { |
| return Collections.emptySet(); |
| } |
| |
| return staticActionBuilders.keySet(); |
| } |
| |
| /** |
| * <p> |
| * Adds another menu identifier to this extender. An extender can represent |
| * many menu identifiers. These identifiers should represent the same menu |
| * manager, selection provider and part. Duplicate identifiers are |
| * automatically ignored. |
| * </p> |
| * <p> |
| * For example, it is necessary to filter out duplicate identifiers for |
| * <code>CompilationUnitEditor</code> instances, as these define both |
| * <code>"#CompilationUnitEditorContext"</code> and |
| * <code>"org.eclipse.jdt.ui.CompilationUnitEditor.EditorContext"</code> |
| * as menu identifier for the same pop-up menu. We don't want to contribute |
| * duplicate items in this case. |
| * </p> |
| * |
| * @param menuId |
| * The menu identifier to add to this extender; should not be |
| * <code>null</code>. |
| */ |
| public final void addMenuId(final String menuId) { |
| bitSet &= ~STATIC_ACTION_READ; |
| if (menuModel != null) { |
| List<String> tags = menuModel.getTags(); |
| String tag = "popup:" + menuId; //$NON-NLS-1$ |
| if (!tags.contains(tag)) { |
| tags.add(tag); |
| } |
| } |
| readStaticActionsFor(menuId); |
| } |
| |
| /** |
| * Determines whether this extender would be the same as another extender |
| * created with the given values. Two extenders are equivalent if they have |
| * the same menu manager, selection provider and part (i.e., if the menu |
| * they represent is about to show, they would populate it with duplicate |
| * values). |
| * |
| * @param menuManager |
| * The menu manager with which to compare; may be |
| * <code>null</code>. |
| * @param selectionProvider |
| * The selection provider with which to compare; may be |
| * <code>null</code>. |
| * @param part |
| * The part with which to compare; may be <code>null</code>. |
| * @return <code>true</code> if the menu manager, selection provider and |
| * part are all the same. |
| */ |
| public final boolean matches(final MenuManager menuManager, |
| final ISelectionProvider selectionProvider, |
| final IWorkbenchPart part) { |
| return (this.menu == menuManager) |
| && (this.selProvider == selectionProvider) |
| && (this.part == part); |
| } |
| |
| /** |
| * Contributes items registered for the currently active editor. |
| */ |
| private void addEditorActions(IMenuManager mgr, Set<IObjectActionContributor> alreadyContributed) { |
| ISelectionProvider activeEditor = new ISelectionProvider() { |
| |
| @Override |
| public void addSelectionChangedListener( |
| ISelectionChangedListener listener) { |
| throw new UnsupportedOperationException( |
| "This ISelectionProvider is static, and cannot be modified."); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public ISelection getSelection() { |
| if (part instanceof IEditorPart) { |
| final IEditorPart editorPart = (IEditorPart) part; |
| return new StructuredSelection(new Object[] { editorPart |
| .getEditorInput() }); |
| } |
| |
| return new StructuredSelection(new Object[0]); |
| } |
| |
| @Override |
| public void removeSelectionChangedListener( |
| ISelectionChangedListener listener) { |
| throw new UnsupportedOperationException( |
| "This ISelectionProvider is static, and cannot be modified."); //$NON-NLS-1$ |
| } |
| |
| @Override |
| public void setSelection(ISelection selection) { |
| throw new UnsupportedOperationException( |
| "This ISelectionProvider is static, and cannot be modified."); //$NON-NLS-1$ |
| } |
| }; |
| |
| if (ObjectActionContributorManager.getManager().contributeObjectActions(part, mgr, |
| activeEditor, alreadyContributed)) { |
| mgr.add(new Separator()); |
| } |
| } |
| |
| /** |
| * Contributes items registered for the object type(s) in |
| * the current selection. |
| */ |
| private void addObjectActions(IMenuManager mgr, Set<IObjectActionContributor> alreadyContributed) { |
| if (selProvider != null) { |
| if (ObjectActionContributorManager.getManager().contributeObjectActions(part, mgr, |
| selProvider, alreadyContributed)) { |
| mgr.add(new Separator()); |
| } |
| } |
| } |
| |
| /** |
| * Disposes all of the static actions. |
| */ |
| private final void clearStaticActions() { |
| bitSet &= ~STATIC_ACTION_READ; |
| if (staticActionBuilders != null) { |
| final Iterator<ViewerActionBuilder> staticActionBuilderItr = staticActionBuilders |
| .values().iterator(); |
| while (staticActionBuilderItr.hasNext()) { |
| final ViewerActionBuilder staticActionBuilder = staticActionBuilderItr.next(); |
| staticActionBuilder.dispose(); |
| } |
| } |
| } |
| |
| /** |
| * Adds static items to the context menu. |
| */ |
| private void addStaticActions(IMenuManager mgr) { |
| if (staticActionBuilders != null) { |
| final Iterator<ViewerActionBuilder> staticActionBuilderItr = staticActionBuilders |
| .values().iterator(); |
| while (staticActionBuilderItr.hasNext()) { |
| final ViewerActionBuilder staticActionBuilder = staticActionBuilderItr.next(); |
| staticActionBuilder.contribute(mgr, null, true); |
| } |
| } |
| } |
| |
| /** |
| * Notifies the listener that the menu is about to be shown. |
| */ |
| @Override |
| public void menuAboutToShow(IMenuManager mgr) { |
| registerE4Support(); |
| |
| // Add this menu as a visible menu. |
| final IWorkbenchPartSite site = part.getSite(); |
| if (site != null) { |
| final IWorkbench workbench = site.getWorkbenchWindow() |
| .getWorkbench(); |
| if (workbench instanceof Workbench) { |
| final Workbench realWorkbench = (Workbench) workbench; |
| runCleanUp(realWorkbench); |
| ISelection input = null; |
| if ((bitSet & INCLUDE_EDITOR_INPUT) != 0) { |
| if (part instanceof IEditorPart) { |
| final IEditorPart editorPart = (IEditorPart) part; |
| input = new StructuredSelection( |
| new Object[] { editorPart.getEditorInput() }); |
| } |
| } |
| ISelection s = (selProvider == null ? null : selProvider |
| .getSelection()); |
| realWorkbench.addShowingMenus(getMenuIds(), s, input); |
| } |
| } |
| |
| addMenuContributions(mgr); |
| |
| readStaticActions(); |
| // test for additions removed to comply with menu contributions |
| if (menuWrapper != null) { |
| mgr = menuWrapper; |
| menuWrapper.removeAll(); |
| } |
| Set<IObjectActionContributor> contributedItems = new HashSet<>(); |
| if ((bitSet & INCLUDE_EDITOR_INPUT) != 0) { |
| addEditorActions(mgr, contributedItems); |
| } |
| addObjectActions(mgr, contributedItems); |
| addStaticActions(mgr); |
| } |
| |
| |
| /** |
| * well, this goes to the renderer. |
| * |
| * @param mgr |
| */ |
| private void addMenuContributions(IMenuManager mgr) { |
| IRendererFactory factory = modelPart.getContext().get(IRendererFactory.class); |
| AbstractPartRenderer obj = factory.getRenderer(menuModel, null); |
| if (obj instanceof MenuManagerRenderer) { |
| MenuManagerRenderer renderer = (MenuManagerRenderer) obj; |
| renderer.reconcileManagerToModel(menu, menuModel); |
| renderer.processContributions(menuModel, menuModel.getElementId(), false, true); |
| // double cast because we're bad people |
| renderer.processContents((MElementContainer<MUIElement>) ((Object) menuModel)); |
| } |
| } |
| |
| private MPopupMenu menuModel; |
| |
| /** |
| * Notifies the listener that the menu is about to be hidden. |
| */ |
| @Override |
| public final void menuAboutToHide(final IMenuManager mgr) { |
| gatherContributions(mgr); |
| cleanupNeeded = true; |
| // Remove this menu as a visible menu. |
| final IWorkbenchPartSite site = part.getSite(); |
| if (site != null) { |
| final IWorkbench workbench = site.getWorkbenchWindow().getWorkbench(); |
| if (workbench instanceof Workbench) { |
| // try delaying this until after the selection event |
| // has been fired. |
| // This is less threatening if the popup: menu |
| // contributions aren't tied to the evaluation service |
| workbench.getDisplay().asyncExec(() -> { |
| final Workbench realWorkbench = (Workbench) workbench; |
| runCleanUp(realWorkbench); |
| }); |
| } |
| } |
| } |
| |
| private void runCleanUp(Workbench realWorkbench) { |
| if (!cleanupNeeded) { |
| return; |
| } |
| cleanupNeeded = false; |
| realWorkbench.removeShowingMenus(getMenuIds(), null, null); |
| cleanUpContributionCache(); |
| } |
| |
| private void gatherContributions(final IMenuManager mgr) { |
| final IContributionItem[] items = mgr.getItems(); |
| for (IContributionItem item : items) { |
| if (item instanceof PluginActionContributionItem) { |
| actionContributionCache.add((PluginActionContributionItem) item); |
| } else if (item instanceof IMenuManager) { |
| gatherContributions(((IMenuManager) item)); |
| } |
| } |
| } |
| |
| private void cleanUpContributionCache() { |
| if (!actionContributionCache.isEmpty()) { |
| PluginActionContributionItem[] items = actionContributionCache |
| .toArray(new PluginActionContributionItem[actionContributionCache.size()]); |
| actionContributionCache.clear(); |
| for (PluginActionContributionItem item : items) { |
| item.dispose(); |
| } |
| } |
| |
| if (modelPart == null || menuModel == null) { |
| return; |
| } |
| IEclipseContext modelContext = modelPart.getContext(); |
| if (modelContext != null) { |
| IRendererFactory factory = modelContext.get(IRendererFactory.class); |
| if (factory != null) { |
| AbstractPartRenderer obj = factory.getRenderer(menuModel, null); |
| if (obj instanceof MenuManagerRenderer) { |
| MenuManagerRenderer renderer = (MenuManagerRenderer) obj; |
| renderer.cleanUp(menuModel); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Read all of the static items for the content menu. |
| */ |
| private final void readStaticActions() { |
| if (staticActionBuilders != null) { |
| final Iterator<String> menuIdItr = staticActionBuilders.keySet().iterator(); |
| while (menuIdItr.hasNext()) { |
| final String menuId = menuIdItr.next(); |
| readStaticActionsFor(menuId); |
| } |
| } |
| } |
| |
| /** |
| * Read static items for a particular menu id, into the context menu. |
| */ |
| private void readStaticActionsFor(final String menuId) { |
| if ((bitSet & STATIC_ACTION_READ) != 0) { |
| return; |
| } |
| |
| bitSet |= STATIC_ACTION_READ; |
| |
| // If no menu id provided, then there is no contributions |
| // to add. Fix for bug #33140. |
| if ((menuId == null) || (menuId.length() < 1)) { |
| return; |
| } |
| |
| if (staticActionBuilders == null) { |
| staticActionBuilders = new HashMap<>(); |
| } |
| |
| Object object = staticActionBuilders.get(menuId); |
| if (!(object instanceof ViewerActionBuilder)) { |
| object = new ViewerActionBuilder(); |
| staticActionBuilders.put(menuId, (ViewerActionBuilder) object); |
| } |
| final ViewerActionBuilder staticActionBuilder = (ViewerActionBuilder) object; |
| staticActionBuilder.readViewerContributions(menuId, selProvider, part); |
| } |
| |
| /** |
| * Dispose of the menu extender. Should only be called when the part |
| * is disposed. |
| */ |
| public void dispose() { |
| clearStaticActions(); |
| Platform.getExtensionRegistry().removeRegistryChangeListener(this); |
| menu.removeMenuListener(this); |
| |
| if (menuModel != null) { |
| // unlink ourselves from the renderer |
| IRendererFactory factory = modelPart.getContext().get(IRendererFactory.class); |
| AbstractPartRenderer obj = factory.getRenderer(menuModel, null); |
| if (obj instanceof MenuManagerRenderer) { |
| MenuManagerRenderer renderer = (MenuManagerRenderer) obj; |
| unlink(renderer, menuModel); |
| renderer.clearModelToManager(menuModel, menu); |
| } |
| |
| modelPart.getMenus().remove(menuModel); |
| } |
| } |
| |
| /** |
| * Unlink all contribution items from the given model menu. |
| * |
| * @param renderer |
| * the renderer that is holding the links |
| * @param menu |
| * the model menu whose children should have its items unlinked |
| * from their corresponding contribution items |
| */ |
| private void unlink(MenuManagerRenderer renderer, MMenu menu) { |
| for (MMenuElement menuElement : menu.getChildren()) { |
| if (OpaqueElementUtil.isOpaqueMenuItem(menuElement) |
| || OpaqueElementUtil.isOpaqueMenuSeparator(menuElement)) { |
| Object item = OpaqueElementUtil.getOpaqueItem(menuElement); |
| if (item instanceof IContributionItem) { |
| renderer.clearModelToContribution(menuElement, (IContributionItem) item); |
| OpaqueElementUtil.clearOpaqueItem(menuElement); |
| } |
| } else if (menuElement instanceof MMenu) { |
| MMenu subMenu = (MMenu) menuElement; |
| unlink(renderer, subMenu); |
| MenuManager manager = renderer.getManager(subMenu); |
| if (manager != null) { |
| renderer.clearModelToManager(subMenu, manager); |
| } |
| } else { |
| IContributionItem contribution = renderer.getContribution(menuElement); |
| if (contribution != null) { |
| renderer.clearModelToContribution(menuElement, contribution); |
| } |
| } |
| } |
| } |
| |
| @Override |
| public void registryChanged(final IRegistryChangeEvent event) { |
| Display display = Display.getDefault(); |
| if (part != null) { |
| display = part.getSite().getPage().getWorkbenchWindow().getWorkbench().getDisplay(); |
| } |
| //check the delta to see if there are any viewer contribution changes. if so, null our builder to cause reparsing on the next menu show |
| IExtensionDelta [] deltas = event.getExtensionDeltas(); |
| for (IExtensionDelta delta : deltas) { |
| IExtensionPoint extensionPoint = delta.getExtensionPoint(); |
| if (extensionPoint.getContributor().getName().equals(WorkbenchPlugin.PI_WORKBENCH) |
| && extensionPoint.getSimpleIdentifier().equals( |
| IWorkbenchRegistryConstants.PL_POPUP_MENU)) { |
| |
| boolean clearPopups = false; |
| IConfigurationElement [] elements = delta.getExtension().getConfigurationElements(); |
| for (IConfigurationElement element : elements) { |
| if (element.getName().equals(IWorkbenchRegistryConstants.TAG_VIEWER_CONTRIBUTION)) { |
| clearPopups = true; |
| break; |
| } |
| } |
| |
| if (clearPopups) { |
| display.syncExec(() -> clearStaticActions()); |
| } |
| } |
| } |
| } |
| |
| public MenuManager getManager() { |
| return menu; |
| } |
| } |