blob: 10cb57b4c4bff663f5d0b803d3c1eca33aab788c [file] [log] [blame]
/*******************************************************************************
* 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));
}
}
}