| /******************************************************************************* |
| * Copyright (c) 2006, 2013 IBM Corporation 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: |
| * IBM Corporation - initial API and implementation |
| *******************************************************************************/ |
| package org.eclipse.debug.internal.ui.views.variables.details; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.Iterator; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| |
| import org.eclipse.core.expressions.EvaluationResult; |
| import org.eclipse.core.expressions.Expression; |
| import org.eclipse.core.expressions.ExpressionConverter; |
| import org.eclipse.core.expressions.ExpressionTagNames; |
| import org.eclipse.core.expressions.IEvaluationContext; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IConfigurationElement; |
| import org.eclipse.core.runtime.IExtensionPoint; |
| import org.eclipse.core.runtime.IStatus; |
| import org.eclipse.core.runtime.Platform; |
| import org.eclipse.core.runtime.Status; |
| import org.eclipse.core.runtime.preferences.IEclipsePreferences; |
| import org.eclipse.core.runtime.preferences.InstanceScope; |
| import org.eclipse.debug.internal.core.IConfigurationElementConstants; |
| import org.eclipse.debug.internal.ui.DebugUIPlugin; |
| import org.eclipse.debug.ui.IDebugUIConstants; |
| import org.eclipse.debug.ui.IDetailPane; |
| import org.eclipse.debug.ui.IDetailPaneFactory; |
| import org.eclipse.jface.viewers.IStructuredSelection; |
| import org.osgi.service.prefs.BackingStoreException; |
| |
| /** |
| * Organizes the detail factories contributed through the extension point and keeps |
| * track of the detail panes the factories produce. Accessed as a singleton through |
| * the <code>getDefault()</code> method. |
| * |
| * @see IDetailPaneFactory |
| * @see IDetailPane |
| * @since 3.3 |
| */ |
| public class DetailPaneManager { |
| |
| /** |
| * Acts as a proxy between the detail pane manager and the factories contributed |
| * to the extension point. Only loads information from the plug-in xml and only |
| * instantiates the specified factory if required (lazy loading). |
| */ |
| private static class DetailPaneFactoryExtension implements IDetailPaneFactory{ |
| |
| private IConfigurationElement fConfigElement; |
| private IDetailPaneFactory fFactory; |
| private Expression fEnablementExpression; |
| |
| public DetailPaneFactoryExtension(IConfigurationElement configElement){ |
| fConfigElement = configElement; |
| } |
| |
| /** |
| * Instantiates the factory and asks it to produce the IDetailPane for |
| * the given ID |
| * @param paneID the identifier of the detail pane to create |
| * @return the new detail pane or <code>null</code> if the backing {@link IDetailPaneFactory} is <code>null</code> |
| */ |
| @Override |
| public IDetailPane createDetailPane(String paneID){ |
| if (getFactory() != null){ |
| return getFactory().createDetailPane(paneID); |
| } |
| return null; |
| } |
| |
| /** |
| * Instantiates the factory and asks it for the set of detail pane |
| * IDs that the factory can produce for the given selection. |
| * @param selection the current view selection |
| * @return the set of detail pane type for the given selection or an empty set, never <code>null</code> |
| */ |
| @Override |
| public Set<String> getDetailPaneTypes(IStructuredSelection selection) { |
| if (getFactory() != null){ |
| return getFactory().getDetailPaneTypes(selection); |
| } |
| return Collections.EMPTY_SET; |
| } |
| |
| /** |
| * Instantiates the factory and asks it for the detail pane ID |
| * that the factory considers the default for the given selection. |
| * @param selection the current view selection |
| * @return the identifier of the default detail pane or <code>null</code> if the backing {@link IDetailPaneFactory} is <code>null</code> |
| */ |
| @Override |
| public String getDefaultDetailPane(IStructuredSelection selection) { |
| if (getFactory() != null){ |
| return getFactory().getDefaultDetailPane(selection); |
| } |
| return null; |
| } |
| |
| /** |
| * Instantiates the factory and asks it to produce the name of the detail pane |
| * for the given ID. |
| * @param paneID the detail pane identifier |
| * @return the name of the detail pane or <code>null</code> if the backing {@link IDetailPaneFactory} is <code>null</code> |
| */ |
| @Override |
| public String getDetailPaneName(String paneID) { |
| if (getFactory() != null){ |
| return getFactory().getDetailPaneName(paneID); |
| } |
| return null; |
| } |
| |
| /** |
| * Instantiates the factory and asks it to produce the description of the |
| * detail pane for the given ID. |
| * @param paneID the detail pane identifier |
| * @return the description of the detail pane or <code>null</code> if the backing {@link IDetailPaneFactory} is <code>null</code> |
| */ |
| @Override |
| public String getDetailPaneDescription(String paneID) { |
| if (getFactory() != null){ |
| return getFactory().getDetailPaneDescription(paneID); |
| } |
| return null; |
| } |
| |
| /** |
| * Returns the instantiated factory specified by the class property. |
| * @return the singleton {@link IDetailPaneFactory} |
| */ |
| private IDetailPaneFactory getFactory(){ |
| if (fFactory != null) { |
| return fFactory; |
| } |
| try{ |
| Object obj = fConfigElement.createExecutableExtension(IConfigurationElementConstants.CLASS); |
| if(obj instanceof IDetailPaneFactory) { |
| fFactory = (IDetailPaneFactory)obj; |
| } else { |
| throw new CoreException(new Status(IStatus.ERROR, DebugUIPlugin.getUniqueIdentifier(), IDebugUIConstants.INTERNAL_ERROR, "org.eclipse.debug.ui.detailFactories extension failed to load a detail factory because the specified class does not implement org.eclipse.debug.ui.IDetailPaneFactory. Class specified was: " + obj, null)); //$NON-NLS-1$ |
| } |
| } catch (CoreException e){ |
| DebugUIPlugin.log(e.getStatus()); |
| fFactory = null; |
| } |
| return fFactory; |
| } |
| |
| /** |
| * Checks if the enablement expression for the factory evaluates to true for the |
| * given selection. |
| * @param selection the current view selection |
| * @return <code>true</code> if the backing {@link IDetailPaneFactory} applies to the given selection, <code>false</code> otherwise |
| */ |
| public boolean isEnabled(IStructuredSelection selection) { |
| boolean enabled = false; |
| // Only the default factory should be enabled for null selections |
| if (selection == null || selection.isEmpty()){ |
| return "org.eclipse.debug.ui.defaultDetailPaneFactory".equals(fConfigElement.getAttribute(IConfigurationElementConstants.ID)); //$NON-NLS-1$ |
| } |
| Expression expression = getEnablementExpression(); |
| if (expression != null) { |
| List<?> list = selection.toList(); |
| IEvaluationContext context = DebugUIPlugin.createEvaluationContext(list); |
| context.addVariable("selection", list); //$NON-NLS-1$ |
| enabled = evalEnablementExpression(context, expression); |
| } else { |
| enabled = true; |
| } |
| return enabled; |
| } |
| |
| /** |
| * Evaluate the given expression within the given context and return |
| * the result. Returns <code>true</code> iff result is either TRUE or NOT_LOADED. |
| * This allows optimistic inclusion of shortcuts before plug-ins are loaded. |
| * Returns <code>false</code> if expression is <code>null</code>. |
| * |
| * @param exp the enablement expression to evaluate or <code>null</code> |
| * @param context the context of the evaluation. Usually, the |
| * user's selection. |
| * @return the result of evaluating the expression |
| */ |
| private boolean evalEnablementExpression(IEvaluationContext context, Expression exp) { |
| try{ |
| if (exp != null){ |
| EvaluationResult result = exp.evaluate(context); |
| if (result == EvaluationResult.TRUE || result == EvaluationResult.NOT_LOADED){ |
| return true; |
| } |
| } |
| } catch (CoreException e){ |
| DebugUIPlugin.log(e.getStatus()); |
| } |
| return false; |
| } |
| |
| /** |
| * Returns an expression that represents the enablement logic for the |
| * detail pane factory or <code>null</code> if none. |
| * |
| * @return an evaluatable expression or <code>null</code> |
| */ |
| private Expression getEnablementExpression(){ |
| // all of this stuff is optional, so...tedious testing is required |
| if (fEnablementExpression == null) { |
| try{ |
| IConfigurationElement[] elements = fConfigElement.getChildren(ExpressionTagNames.ENABLEMENT); |
| IConfigurationElement enablement = elements.length > 0 ? elements[0] : null; |
| if (enablement != null) { |
| fEnablementExpression = ExpressionConverter.getDefault().perform(enablement); |
| } |
| } catch (CoreException e){ |
| DebugUIPlugin.log(e.getStatus()); |
| fEnablementExpression = null; |
| } |
| } |
| return fEnablementExpression; |
| } |
| |
| } |
| |
| /** |
| * There should only ever be once instance of this manager for the workbench. |
| */ |
| private static DetailPaneManager fgSingleton; |
| |
| /** |
| * Maps the IDs of types of detail panes to the factory that can create them. |
| * There can currently only be one factory for a given type of details pane. |
| */ |
| private Map<String, IDetailPaneFactory> fFactoriesByPaneID; |
| |
| /** |
| * Maps a Set of detail pane id's to the one detail pane id that is preferred. |
| */ |
| private Map<Set<String>, String> fPreferredDetailPanes; |
| |
| /** |
| * The set of all factories that have been loaded from the extension point. |
| */ |
| private List<DetailPaneFactoryExtension> fKnownFactories; |
| |
| /** |
| * Preference key for storing the preferred detail panes map. |
| * @see #storePreferredDetailsAreas() |
| * @see #loadPreferredDetailsAreas() |
| */ |
| public static final String PREF_DETAIL_AREAS = "preferredDetailPanes"; //$NON-NLS-1$ |
| |
| private DetailPaneManager(){ |
| fFactoriesByPaneID = new HashMap<>(); |
| fFactoriesByPaneID.put(MessageDetailPane.ID, new DefaultDetailPaneFactory()); |
| } |
| |
| public static DetailPaneManager getDefault(){ |
| if (fgSingleton == null) { |
| fgSingleton = new DetailPaneManager(); |
| } |
| return fgSingleton; |
| } |
| |
| /** |
| * Returns the ID of the preferred detail pane for the given selection. |
| * |
| * @param selection The selection to display in the detail pane |
| * @return The ID of the preferred detail pane or null |
| */ |
| public String getPreferredPaneFromSelection(IStructuredSelection selection){ |
| List<IDetailPaneFactory> possibleFactories = getEnabledFactories(selection); |
| Set<String> possiblePaneIDs = getPossiblePaneIDs(possibleFactories, selection); |
| return chooseDetailsAreaIDInSet(possiblePaneIDs, possibleFactories, selection); |
| } |
| |
| /** |
| * Returns the set of all possible detail panes the can display the given |
| * selection. |
| * |
| * @param selection The selection to display in the detail pane |
| * @return The set of IDs of all possible detail panes for the given selection |
| */ |
| public Set<String> getAvailablePaneIDs(IStructuredSelection selection) { |
| List<IDetailPaneFactory> possibleFactories = getEnabledFactories(selection); |
| return getPossiblePaneIDs(possibleFactories, selection); |
| } |
| |
| /** |
| * Given the ID of a details pane, this method will try to find the factory |
| * that creates it and return an instantiation of that area. |
| * <p> |
| * This method will not call the init() method of the IDetailsPane. |
| * </p> |
| * |
| * @param ID The ID of the requested pane |
| * @return The instantiated pane or null |
| */ |
| public IDetailPane getDetailPaneFromID(String ID){ |
| IDetailPaneFactory factory = fFactoriesByPaneID.get(ID); |
| if (factory != null){ |
| return factory.createDetailPane(ID); |
| } |
| return null; |
| } |
| |
| /** |
| * Given the ID of a details pane, this method will try to find the factory |
| * that creates it and ask it for the name of the details pane. |
| * |
| * @param ID The ID of the requested pane |
| * @return The name of the details pane or null |
| */ |
| public String getNameFromID(String ID){ |
| IDetailPaneFactory factory = fFactoriesByPaneID.get(ID); |
| if (factory != null){ |
| return factory.getDetailPaneName(ID); |
| } |
| return null; |
| } |
| |
| /** |
| * Given the ID of a details pane, this method will try to find the factory |
| * that creates it and ask it for the description of the details pane. |
| * |
| * @param ID The ID of the requested pane |
| * @return The description of the details pane or null |
| */ |
| public String getDescriptionFromID(String ID){ |
| IDetailPaneFactory factory = fFactoriesByPaneID.get(ID); |
| if (factory != null){ |
| return factory.getDetailPaneDescription(ID); |
| } |
| return null; |
| } |
| |
| |
| /** |
| * Returns the set of IDetailPaneFactories (they will be DetailPaneFactoryDelegates) that were |
| * contributed to the extension point and are enabled for the given selection |
| * (enabled if the factory does not have an enablement expression or if the |
| * enablement expression evaluates to true). |
| * @param selection the current view selection |
| * |
| * @return The factories enabled for the selection or an empty collection. |
| */ |
| private List<IDetailPaneFactory> getEnabledFactories(IStructuredSelection selection) { |
| List<IDetailPaneFactory> factoriesForSelection = new ArrayList<>(); |
| if (fKnownFactories == null) { |
| initializeDetailFactories(); |
| } |
| for (IDetailPaneFactory currentFactory : fKnownFactories) { |
| if (currentFactory instanceof DetailPaneFactoryExtension){ |
| if (((DetailPaneFactoryExtension)currentFactory).isEnabled(selection)){ |
| factoriesForSelection.add(currentFactory); |
| } |
| } |
| } |
| return factoriesForSelection; |
| } |
| |
| /** |
| * Produces the set of IDs for all possible detail panes that can be used to display |
| * the given selection. |
| * |
| * @param factoriesToQuery The collection of factories to check |
| * @param selection The selection to be displayed |
| * @return Set of pane IDs or an empty set |
| */ |
| private Set<String> getPossiblePaneIDs(List<IDetailPaneFactory> factoriesToQuery, IStructuredSelection selection) { |
| Set<String> idsForSelection = new LinkedHashSet<>(); |
| for (IDetailPaneFactory currentFactory : factoriesToQuery) { |
| for (String currentAreaTypeID : currentFactory.getDetailPaneTypes(selection)) { |
| fFactoriesByPaneID.put(currentAreaTypeID, currentFactory); |
| idsForSelection.add(currentAreaTypeID); |
| } |
| } |
| return idsForSelection; |
| } |
| |
| /** |
| * Given a set of possible detail pane IDs, this method will determine which pane is |
| * preferred and should be used to display the selection. This method chooses a pane |
| * by storing previous choices and can be set using a context menu. |
| * |
| * @param possiblePaneIDs The set of possible detail pane IDs |
| * @param enabledFactories the complete listing of enable {@link IDetailPaneFactory}s |
| * @param selection the current selection from the variables view |
| * @return The preferred detail pane ID or null |
| */ |
| private String chooseDetailsAreaIDInSet(Set<String> possiblePaneIDs, List<IDetailPaneFactory> enabledFactories, IStructuredSelection selection) { |
| if (possiblePaneIDs == null || possiblePaneIDs.isEmpty()){ |
| return null; |
| } |
| |
| String preferredID = getUserPreferredDetailPane(possiblePaneIDs); |
| |
| if (preferredID == null){ |
| // If there is no preferred pane already set, check the factories to see there is a default pane |
| for (IDetailPaneFactory currentFactory : enabledFactories) { |
| preferredID = currentFactory.getDefaultDetailPane(selection); |
| if (preferredID != null) { |
| break; |
| } |
| } |
| // If the factories don't have a default, try to choose the DefaultDetailPane |
| if (preferredID == null){ |
| Iterator<String> paneIterator = possiblePaneIDs.iterator(); |
| // If the DefaultDetailPane is not in the set, just use the first in the set |
| preferredID = paneIterator.next(); |
| while (paneIterator.hasNext() && preferredID != DefaultDetailPaneFactory.DEFAULT_DETAIL_PANE_ID) { |
| String currentID = paneIterator.next(); |
| if (currentID.equals(DefaultDetailPaneFactory.DEFAULT_DETAIL_PANE_ID)){ |
| preferredID = currentID; |
| } |
| } |
| } |
| setPreferredDetailPane(possiblePaneIDs, preferredID); |
| } |
| |
| return preferredID; |
| } |
| |
| /** |
| * Initializes the collection of known factories from extension point contributions. |
| */ |
| private synchronized void initializeDetailFactories(){ |
| if (fKnownFactories == null){ |
| fKnownFactories = new ArrayList<>(); |
| IExtensionPoint extensionPoint = Platform.getExtensionRegistry().getExtensionPoint(DebugUIPlugin.getUniqueIdentifier(), IDebugUIConstants.EXTENSION_POINT_DETAIL_FACTORIES); |
| IConfigurationElement[] infos = extensionPoint.getConfigurationElements(); |
| DetailPaneFactoryExtension delegate = null; |
| for (IConfigurationElement info : infos) { |
| delegate = new DetailPaneFactoryExtension(info); |
| fKnownFactories.add(delegate); |
| } |
| } |
| } |
| |
| /** |
| * Returns the preferred pane ID from the given set if the mapping has been set. |
| * |
| * @param possibleDetailsAreaIDs Set of possible pane IDs |
| * @return The preferred ID or null |
| */ |
| public String getUserPreferredDetailPane(Set<String> possibleDetailsAreaIDs) { |
| if (fPreferredDetailPanes == null){ |
| loadPreferredDetailsAreas(); |
| } |
| return fPreferredDetailPanes.get(possibleDetailsAreaIDs); |
| |
| } |
| |
| /** |
| * Adds or updates the mapping to set which pane ID is preferred for a certain |
| * set of possible IDs. |
| * |
| * @param possibleDetailsAreaIDs The set of possible IDs |
| * @param preferredDetailsAreaID The preferred ID in the set. |
| */ |
| public void setPreferredDetailPane(Set<String> possibleDetailsAreaIDs, String preferredDetailsAreaID) { |
| if (possibleDetailsAreaIDs == null) { |
| return; |
| } |
| if (fPreferredDetailPanes == null){ |
| loadPreferredDetailsAreas(); |
| } |
| String currentKey = fPreferredDetailPanes.get(possibleDetailsAreaIDs); |
| if (currentKey == null || !currentKey.equals(preferredDetailsAreaID)){ |
| fPreferredDetailPanes.put(possibleDetailsAreaIDs, preferredDetailsAreaID); |
| storePreferredDetailsAreas(); |
| } |
| |
| } |
| |
| /** |
| * Stores the map of preferred detail pane IDs to the preference store in the format: |
| * |
| * Key1A,Key1B:Value1|Key2A,Key2B,Key2C:Value2| |
| * |
| * Where the sub keys (Key1A, Key1B, etc.) are the elements of the set used at the |
| * key in the mapping and the values are the associated String value in the mapping. |
| */ |
| private void storePreferredDetailsAreas() { |
| StringBuilder buffer= new StringBuilder(); |
| for (Entry<Set<String>, String> entry : fPreferredDetailPanes.entrySet()) { |
| for (String currentID : entry.getKey()) { |
| buffer.append(currentID); |
| buffer.append(','); |
| } |
| buffer.deleteCharAt(buffer.length()-1); |
| buffer.append(':'); |
| buffer.append(entry.getValue()); |
| buffer.append('|'); |
| } |
| IEclipsePreferences node = InstanceScope.INSTANCE.getNode(DebugUIPlugin.getUniqueIdentifier()); |
| if(node != null) { |
| node.put(PREF_DETAIL_AREAS, buffer.toString()); |
| try { |
| node.flush(); |
| } catch (BackingStoreException e) { |
| DebugUIPlugin.log(e); |
| } |
| } |
| } |
| |
| /** |
| * Loads the map of preferred detail pane IDs from the preference store. |
| * |
| * @see #storePreferredDetailsAreas() |
| */ |
| private void loadPreferredDetailsAreas() { |
| fPreferredDetailPanes = new HashMap<>(); |
| String preferenceValue = Platform.getPreferencesService().getString(DebugUIPlugin.getUniqueIdentifier(), |
| PREF_DETAIL_AREAS, |
| "", //$NON-NLS-1$ |
| null); |
| StringTokenizer entryTokenizer = new StringTokenizer(preferenceValue,"|"); //$NON-NLS-1$ |
| while (entryTokenizer.hasMoreTokens()){ |
| String token = entryTokenizer.nextToken(); |
| int valueStart = token.indexOf(':'); |
| StringTokenizer keyTokenizer = new StringTokenizer(token.substring(0,valueStart),","); //$NON-NLS-1$ |
| Set<String> keys = new LinkedHashSet<>(); |
| while (keyTokenizer.hasMoreTokens()){ |
| keys.add(keyTokenizer.nextToken()); |
| } |
| fPreferredDetailPanes.put(keys, token.substring(valueStart+1)); |
| } |
| } |
| |
| } |