blob: c1758c930f7cf0dc01407ad60c78658dcd556ee4 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2008 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
*******************************************************************************/
package org.eclipse.ui.internal.contexts;
import java.util.ArrayList;
import java.util.Collection;
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 java.util.WeakHashMap;
import org.eclipse.core.commands.contexts.ContextManager;
import org.eclipse.core.commands.util.Tracing;
import org.eclipse.core.expressions.Expression;
import org.eclipse.core.runtime.Assert;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.ActiveShellExpression;
import org.eclipse.ui.ISources;
import org.eclipse.ui.contexts.IContextActivation;
import org.eclipse.ui.contexts.IContextService;
import org.eclipse.ui.internal.misc.Policy;
import org.eclipse.ui.internal.services.ExpressionAuthority;
/**
* <p>
* A central authority for deciding activation of contexts. This authority
* listens to a variety of incoming sources, and updates the underlying context
* manager if changes occur.
* </p>
*
* @since 3.1
*/
public final class ContextAuthority extends ExpressionAuthority {
/**
* The default size of the set containing the activations to recompute. This
* is more than enough to cover the average case.
*/
private static final int ACTIVATIONS_TO_RECOMPUTE_SIZE = 4;
/**
* Whether the context authority should kick into debugging mode. This
* causes the unresolvable handler conflicts to be printed to the console.
*/
private static final boolean DEBUG = Policy.DEBUG_CONTEXTS;
/**
* Whether the performance information should be printed about the
* performance of the context authority.
*/
private static final boolean DEBUG_PERFORMANCE = Policy.DEBUG_CONTEXTS_PERFORMANCE;
/**
* The name of the data tag containing the dispose listener information.
*/
private static final String DISPOSE_LISTENER = "org.eclipse.ui.internal.contexts.ContextAuthority"; //$NON-NLS-1$
/**
* The component name to print when displaying tracing information.
*/
private static final String TRACING_COMPONENT = "CONTEXTS"; //$NON-NLS-1$
/**
* A bucket sort of the context activations based on source priority. Each
* activation will appear only once per set, but may appear in multiple
* sets. If no activations are defined for a particular priority level, then
* the array at that index will only contain <code>null</code>.
*/
private final Set[] activationsBySourcePriority = new Set[33];
/**
* This is a map of context activations (<code>Collection</code> of
* <code>IContextActivation</code>) sorted by context identifier (<code>String</code>).
* If there is only one context activation for a context, then the
* <code>Collection</code> is replaced by a
* <code>IContextActivation</code>. If there is no activation, the entry
* should be removed entirely.
*/
private final Map contextActivationsByContextId = new HashMap();
/**
* The context manager that should be updated when the contexts are
* changing.
*/
private final ContextManager contextManager;
/**
* The context service that should be used for authority-managed
* shell-related contexts. This value is never <code>null</code>.
*/
private final IContextService contextService;
/**
* This is a map of shell to a list of activations. When a shell is
* registered, it is added to this map with the list of activation that
* should be submitted when the shell is active. When the shell is
* deactivated, this same list should be withdrawn. A shell is removed from
* this map using the {@link #unregisterShell(Shell)}method. This value may
* be empty, but is never <code>null</code>. The <code>null</code> key
* is reserved for active shells that have not been registered but have a
* parent (i.e., default dialog service).
*/
private final Map registeredWindows = new WeakHashMap();
/**
* Constructs a new instance of <code>ContextAuthority</code>.
*
* @param contextManager
* The context manager from which contexts can be retrieved (to
* update their active state); must not be <code>null</code>.
* @param contextService
* The workbench context service for which this authority is
* acting. This allows the authority to manage shell-specific
* contexts. This value must not be <code>null</code>.
*/
ContextAuthority(final ContextManager contextManager,
final IContextService contextService) {
if (contextManager == null) {
throw new NullPointerException(
"The context authority needs a context manager"); //$NON-NLS-1$
}
if (contextService == null) {
throw new NullPointerException(
"The context authority needs an evaluation context"); //$NON-NLS-1$
}
this.contextManager = contextManager;
this.contextService = contextService;
}
/**
* Activates a context on the workbench. This will add it to a master list.
*
* @param activation
* The activation; must not be <code>null</code>.
*/
final void activateContext(final IContextActivation activation) {
// First we update the contextActivationsByContextId map.
final String contextId = activation.getContextId();
final Object value = contextActivationsByContextId.get(contextId);
if (value instanceof Collection) {
final Collection contextActivations = (Collection) value;
if (!contextActivations.contains(activation)) {
contextActivations.add(activation);
updateContext(contextId, containsActive(contextActivations));
}
} else if (value instanceof IContextActivation) {
if (value != activation) {
final Collection contextActivations = new ArrayList(2);
contextActivations.add(value);
contextActivations.add(activation);
contextActivationsByContextId
.put(contextId, contextActivations);
updateContext(contextId, containsActive(contextActivations));
}
} else {
contextActivationsByContextId.put(contextId, activation);
updateContext(contextId, evaluate(activation));
}
// Next we update the source priority bucket sort of activations.
final int sourcePriority = activation.getSourcePriority();
for (int i = 1; i <= 32; i++) {
if ((sourcePriority & (1 << i)) != 0) {
Set activations = activationsBySourcePriority[i];
if (activations == null) {
activations = new HashSet(1);
activationsBySourcePriority[i] = activations;
}
activations.add(activation);
}
}
}
/**
* Checks whether the new active shell is registered. If it is already
* registered, then it does no work. If it is not registered, then it checks
* what type of contexts the shell should have by default. This is
* determined by parenting. A shell with no parent receives no contexts. A
* shell with a parent, receives the dialog contexts.
*
* @param newShell
* The newly active shell; may be <code>null</code> or
* disposed.
* @param oldShell
* The previously active shell; may be <code>null</code> or
* disposed.
*/
private final void checkWindowType(final Shell newShell,
final Shell oldShell) {
/*
* If the previous active shell was recognized as a dialog by default,
* then remove its submissions.
*/
Collection oldActivations = (Collection) registeredWindows
.get(oldShell);
if (oldActivations == null) {
/*
* The old shell wasn't registered. So, we need to check if it was
* considered a dialog by default.
*/
oldActivations = (Collection) registeredWindows.get(null);
if (oldActivations != null) {
final Iterator oldActivationItr = oldActivations.iterator();
while (oldActivationItr.hasNext()) {
final IContextActivation activation = (IContextActivation) oldActivationItr
.next();
deactivateContext(activation);
}
}
}
/*
* If the new active shell is recognized as a dialog by default, then
* create some submissions, remember them, and submit them for
* processing.
*/
if ((newShell != null) && (!newShell.isDisposed())) {
final Collection newActivations;
if ((newShell.getParent() != null)
&& (registeredWindows.get(newShell) == null)) {
// This is a dialog by default.
newActivations = new ArrayList();
final Expression expression = new ActiveShellExpression(
newShell);
final IContextActivation dialogWindowActivation = new ContextActivation(
IContextService.CONTEXT_ID_DIALOG_AND_WINDOW,
expression, contextService);
activateContext(dialogWindowActivation);
newActivations.add(dialogWindowActivation);
final IContextActivation dialogActivation = new ContextActivation(
IContextService.CONTEXT_ID_DIALOG, expression,
contextService);
activateContext(dialogActivation);
newActivations.add(dialogActivation);
registeredWindows.put(null, newActivations);
/*
* Make sure the submissions will be removed in event of
* disposal. This is really just a paranoid check. The
* "oldSubmissions" code above should take care of this.
*/
newShell.addDisposeListener(new DisposeListener() {
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
*/
public void widgetDisposed(DisposeEvent e) {
registeredWindows.remove(null);
if (!newShell.isDisposed()) {
newShell.removeDisposeListener(this);
}
/*
* In the case where a dispose has happened, we are
* expecting an activation event to arrive at some point
* in the future. If we process the submissions now,
* then we will update the activeShell before
* checkWindowType is called. This means that dialogs
* won't be recognized as dialogs.
*/
final Iterator newActivationItr = newActivations
.iterator();
while (newActivationItr.hasNext()) {
deactivateContext((IContextActivation) newActivationItr
.next());
}
}
});
} else {
// Shells that are not dialogs by default must register.
newActivations = null;
}
}
}
/**
* Returns a subset of the given <code>activations</code> containing only
* those that are active
*
* @param activations
* The activations to trim; must not be <code>null</code>, but
* may be empty.
* @return <code>true</code> if there is at least one active context;
* <code>false</code> otherwise.
*/
private final boolean containsActive(final Collection activations) {
final Iterator activationItr = activations.iterator();
while (activationItr.hasNext()) {
final IContextActivation activation = (IContextActivation) activationItr
.next();
if (evaluate(activation)) {
return true;
}
}
return false;
}
/**
* Removes an activation for a context on the workbench. This will remove it
* from the master list, and update the appropriate context, if necessary.
*
* @param activation
* The activation; must not be <code>null</code>.
*/
final void deactivateContext(final IContextActivation activation) {
// First we update the handlerActivationsByCommandId map.
final String contextId = activation.getContextId();
final Object value = contextActivationsByContextId.get(contextId);
if (value instanceof Collection) {
final Collection contextActivations = (Collection) value;
if (contextActivations.contains(activation)) {
contextActivations.remove(activation);
if (contextActivations.isEmpty()) {
contextActivationsByContextId.remove(contextId);
updateContext(contextId, false);
} else if (contextActivations.size() == 1) {
final IContextActivation remainingActivation = (IContextActivation) contextActivations
.iterator().next();
contextActivationsByContextId.put(contextId,
remainingActivation);
updateContext(contextId, evaluate(remainingActivation));
} else {
updateContext(contextId, containsActive(contextActivations));
}
}
} else if (value instanceof IContextActivation) {
if (value == activation) {
contextActivationsByContextId.remove(contextId);
updateContext(contextId, false);
}
}
// Next we update the source priority bucket sort of activations.
final int sourcePriority = activation.getSourcePriority();
for (int i = 1; i <= 32; i++) {
if ((sourcePriority & (1 << i)) != 0) {
final Set activations = activationsBySourcePriority[i];
if (activations == null) {
continue;
}
activations.remove(activation);
if (activations.isEmpty()) {
activationsBySourcePriority[i] = null;
}
}
}
}
/**
* Returns the currently active shell.
*
* @return The currently active shell; may be <code>null</code>.
*/
final Shell getActiveShell() {
return (Shell) getVariable(ISources.ACTIVE_SHELL_NAME);
}
/**
* Returns the shell type for the given shell.
*
* @param shell
* The shell for which the type should be determined. If this
* value is <code>null</code>, then
* <code>IWorkbenchContextSupport.TYPE_NONE</code> is returned.
* @return <code>IWorkbenchContextSupport.TYPE_WINDOW</code>,
* <code>IWorkbenchContextSupport.TYPE_DIALOG</code>, or
* <code>IWorkbenchContextSupport.TYPE_NONE</code>.
*/
public final int getShellType(final Shell shell) {
// If the shell is null, then return none.
if (shell == null) {
return IContextService.TYPE_NONE;
}
final Collection activations = (Collection) registeredWindows
.get(shell);
if (activations != null) {
// The shell is registered, so check what type it was registered as.
if (activations.isEmpty()) {
// It was registered as none.
return IContextService.TYPE_NONE;
}
// Look for the right type of context id.
final Iterator activationItr = activations.iterator();
while (activationItr.hasNext()) {
final IContextActivation activation = (IContextActivation) activationItr
.next();
final String contextId = activation.getContextId();
if (contextId == IContextService.CONTEXT_ID_DIALOG) {
return IContextService.TYPE_DIALOG;
} else if (contextId == IContextService.CONTEXT_ID_WINDOW) {
return IContextService.TYPE_WINDOW;
}
}
// This shouldn't be possible.
Assert
.isTrue(
false,
"A registered shell should have at least one submission matching TYPE_WINDOW or TYPE_DIALOG"); //$NON-NLS-1$
return IContextService.TYPE_NONE; // not reachable
} else if (shell.getParent() != null) {
/*
* The shell is not registered, but it has a parent. It is therefore
* considered a dialog by default.
*/
return IContextService.TYPE_DIALOG;
} else {
/*
* The shell is not registered, but has no parent. It gets no key
* bindings.
*/
return IContextService.TYPE_NONE;
}
}
/**
* <p>
* Registers a shell to automatically promote or demote some basic types of
* contexts. The "In Dialogs" and "In Windows" contexts are provided by the
* system. This a convenience method to ensure that these contexts are
* promoted when the given is shell is active.
* </p>
* <p>
* If a shell is registered as a window, then the "In Windows" context is
* enabled when that shell is active. If a shell is registered as a dialog --
* or is not registered, but has a parent shell -- then the "In Dialogs"
* context is enabled when that shell is active. If the shell is registered
* as none -- or is not registered, but has no parent shell -- then the
* neither of the contexts will be enabled (by us -- someone else can always
* enabled them).
* </p>
* <p>
* If the provided shell has already been registered, then this method will
* change the registration.
* </p>
*
* @param shell
* The shell to register for key bindings; must not be
* <code>null</code>.
* @param type
* The type of shell being registered. This value must be one of
* the constants given in this interface.
*
* @return <code>true</code> if the shell had already been registered
* (i.e., the registration has changed); <code>false</code>
* otherwise.
*/
public final boolean registerShell(final Shell shell, final int type) {
// We do not allow null shell registration. It is reserved.
if (shell == null) {
throw new NullPointerException("The shell was null"); //$NON-NLS-1$
}
// Debugging output
if (DEBUG) {
final StringBuffer buffer = new StringBuffer("register shell '"); //$NON-NLS-1$
buffer.append(shell);
buffer.append("' as "); //$NON-NLS-1$
switch (type) {
case IContextService.TYPE_DIALOG:
buffer.append("dialog"); //$NON-NLS-1$
break;
case IContextService.TYPE_WINDOW:
buffer.append("window"); //$NON-NLS-1$
break;
case IContextService.TYPE_NONE:
buffer.append("none"); //$NON-NLS-1$
break;
default:
buffer.append("unknown"); //$NON-NLS-1$
break;
}
Tracing.printTrace(TRACING_COMPONENT, buffer.toString());
}
// Build the list of submissions.
final List activations = new ArrayList();
Expression expression;
IContextActivation dialogWindowActivation;
switch (type) {
case IContextService.TYPE_DIALOG:
expression = new ActiveShellExpression(shell);
dialogWindowActivation = new ContextActivation(
IContextService.CONTEXT_ID_DIALOG_AND_WINDOW, expression,
contextService);
activateContext(dialogWindowActivation);
activations.add(dialogWindowActivation);
final IContextActivation dialogActivation = new ContextActivation(
IContextService.CONTEXT_ID_DIALOG, expression,
contextService);
activateContext(dialogActivation);
activations.add(dialogActivation);
break;
case IContextService.TYPE_NONE:
break;
case IContextService.TYPE_WINDOW:
expression = new ActiveShellExpression(shell);
dialogWindowActivation = new ContextActivation(
IContextService.CONTEXT_ID_DIALOG_AND_WINDOW, expression,
contextService);
activateContext(dialogWindowActivation);
activations.add(dialogWindowActivation);
final IContextActivation windowActivation = new ContextActivation(
IContextService.CONTEXT_ID_WINDOW, expression,
contextService);
activateContext(windowActivation);
activations.add(windowActivation);
break;
default:
throw new IllegalArgumentException("The type is not recognized: " //$NON-NLS-1$
+ type);
}
// Check to see if the activations are already present.
boolean returnValue = false;
final Collection previousActivations = (Collection) registeredWindows
.get(shell);
if (previousActivations != null) {
returnValue = true;
final Iterator previousActivationItr = previousActivations
.iterator();
while (previousActivationItr.hasNext()) {
final IContextActivation activation = (IContextActivation) previousActivationItr
.next();
deactivateContext(activation);
}
}
// Add the new submissions, and force some reprocessing to occur.
registeredWindows.put(shell, activations);
/*
* Remember the dispose listener so that we can remove it later if we
* unregister the shell.
*/
final DisposeListener shellDisposeListener = new DisposeListener() {
/*
* (non-Javadoc)
*
* @see org.eclipse.swt.events.DisposeListener#widgetDisposed(org.eclipse.swt.events.DisposeEvent)
*/
public void widgetDisposed(DisposeEvent e) {
registeredWindows.remove(shell);
if (!shell.isDisposed()) {
shell.removeDisposeListener(this);
}
/*
* In the case where a dispose has happened, we are expecting an
* activation event to arrive at some point in the future. If we
* process the submissions now, then we will update the
* activeShell before checkWindowType is called. This means that
* dialogs won't be recognized as dialogs.
*/
final Iterator activationItr = activations.iterator();
while (activationItr.hasNext()) {
deactivateContext((IContextActivation) activationItr.next());
}
}
};
// Make sure the submissions will be removed in event of disposal.
shell.addDisposeListener(shellDisposeListener);
shell.setData(DISPOSE_LISTENER, shellDisposeListener);
return returnValue;
}
/**
* Carries out the actual source change notification. It assumed that by the
* time this method is called, <code>context</code> is up-to-date with the
* current state of the application.
*
* @param sourcePriority
* A bit mask of all the source priorities that have changed.
*/
protected final void sourceChanged(final int sourcePriority) {
// If tracing, then track how long it takes to process the activations.
long startTime = 0L;
if (DEBUG_PERFORMANCE) {
startTime = System.currentTimeMillis();
}
/*
* In this first phase, we cycle through all of the activations that
* could have potentially changed. Each such activation is added to a
* set for future processing. We add it to a set so that we avoid
* handling any individual activation more than once.
*/
final Set activationsToRecompute = new HashSet(
ACTIVATIONS_TO_RECOMPUTE_SIZE);
for (int i = 1; i <= 32; i++) {
if ((sourcePriority & (1 << i)) != 0) {
final Collection activations = activationsBySourcePriority[i];
if (activations != null) {
final Iterator activationItr = activations.iterator();
while (activationItr.hasNext()) {
activationsToRecompute.add(activationItr.next());
}
}
}
}
/*
* For every activation, we recompute its active state, and check
* whether it has changed. If it has changed, then we take note of the
* context identifier so we can update the context later.
*/
final Collection changedContextIds = new ArrayList(
activationsToRecompute.size());
final Iterator activationItr = activationsToRecompute.iterator();
while (activationItr.hasNext()) {
final IContextActivation activation = (IContextActivation) activationItr
.next();
final boolean currentActive = evaluate(activation);
activation.clearResult();
final boolean newActive = evaluate(activation);
if (newActive != currentActive) {
changedContextIds.add(activation.getContextId());
}
}
try {
contextManager.deferUpdates(true);
/*
* For every context identifier with a changed activation, we
* resolve conflicts and trigger an update.
*/
final Iterator changedContextIdItr = changedContextIds.iterator();
while (changedContextIdItr.hasNext()) {
final String contextId = (String) changedContextIdItr.next();
final Object value = contextActivationsByContextId
.get(contextId);
if (value instanceof IContextActivation) {
final IContextActivation activation = (IContextActivation) value;
updateContext(contextId, evaluate(activation));
} else if (value instanceof Collection) {
updateContext(contextId, containsActive((Collection) value));
} else {
updateContext(contextId, false);
}
}
} finally {
contextManager.deferUpdates(false);
}
// If tracing performance, then print the results.
if (DEBUG_PERFORMANCE) {
final long elapsedTime = System.currentTimeMillis() - startTime;
final int size = activationsToRecompute.size();
if (size > 0) {
Tracing.printTrace(TRACING_COMPONENT, size
+ " activations recomputed in " + elapsedTime + "ms"); //$NON-NLS-1$ //$NON-NLS-2$
}
}
}
/**
* <p>
* Unregisters a shell that was previously registered. After this method
* completes, the shell will be treated as if it had never been registered
* at all. If you have registered a shell, you should ensure that this
* method is called when the shell is disposed. Otherwise, a potential
* memory leak will exist.
* </p>
* <p>
* If the shell was never registered, or if the shell is <code>null</code>,
* then this method returns <code>false</code> and does nothing.
*
* @param shell
* The shell to be unregistered; does nothing if this value is
* <code>null</code>.
*
* @return <code>true</code> if the shell had been registered;
* <code>false</code> otherwise.
*/
public final boolean unregisterShell(final Shell shell) {
// Don't allow this method to play with the special null slot.
if (shell == null) {
return false;
}
/*
* If we're unregistering the shell but we're not about to dispose it,
* then we'll end up leaking the DisposeListener unless we remove it
* here.
*/
if (!shell.isDisposed()) {
final DisposeListener oldListener = (DisposeListener) shell
.getData(DISPOSE_LISTENER);
if (oldListener != null) {
shell.removeDisposeListener(oldListener);
}
}
Collection previousActivations = (Collection) registeredWindows
.get(shell);
if (previousActivations != null) {
registeredWindows.remove(shell);
final Iterator previousActivationItr = previousActivations
.iterator();
while (previousActivationItr.hasNext()) {
final IContextActivation activation = (IContextActivation) previousActivationItr
.next();
deactivateContext(activation);
}
return true;
}
return false;
}
/**
* Updates the context with the given context activation.
*
* @param contextId
* The identifier of the context which should be updated; must
* not be <code>null</code>.
* @param active
* Whether the context should be active; <code>false</code>
* otherwise.
*/
private final void updateContext(final String contextId,
final boolean active) {
if (active) {
contextManager.addActiveContext(contextId);
} else {
contextManager.removeActiveContext(contextId);
}
}
/**
* Updates this authority's evaluation context. If the changed variable is
* the <code>ISources.ACTIVE_SHELL_NAME</code> variable, then this also
* triggers an update of the shell-specific contexts. For example, if a
* dialog becomes active, then the dialog context will be activated by this
* method.
*
* @param name
* The name of the variable to update; must not be
* <code>null</code>.
* @param value
* The new value of the variable. If this value is
* <code>null</code>, then the variable is removed.
*/
protected final void updateEvaluationContext(final String name,
final Object value) {
/*
* Bug 84056. If we update the active workbench window, then we risk
* falling back to that shell when the active shell has registered as
* "none".
*/
if ((name != null)
&& (!ISources.ACTIVE_WORKBENCH_WINDOW_SHELL_NAME.equals(name))) {
/*
* We need to track shell activation ourselves, as some special
* contexts are automatically activated in response to different
* types of shells becoming active.
*/
if (ISources.ACTIVE_SHELL_NAME.equals(name)) {
checkWindowType((Shell) value,
(Shell) getVariable(ISources.ACTIVE_SHELL_NAME));
}
// Update the evaluation context itself.
changeVariable(name, value);
}
}
/**
* <p>
* Bug 95792. A mechanism by which the key binding architecture can force an
* update of the contexts (based on the active shell) before trying to
* execute a command. This mechanism is required for GTK+ only.
* </p>
* <p>
* DO NOT CALL THIS METHOD.
* </p>
*/
final void updateShellKludge() {
updateCurrentState();
sourceChanged(ISources.ACTIVE_SHELL);
}
}