blob: 6e5ff9043b9e72026a08581e1ae1d4e1d0839498 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2015 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.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<IContextActivation>[] 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<String, Object> 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<Shell, Collection<IContextActivation>> 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>.
*/
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<IContextActivation> contextActivations = (Collection<IContextActivation>) value;
if (!contextActivations.contains(activation)) {
contextActivations.add(activation);
updateContext(contextId, containsActive(contextActivations));
}
} else if (value instanceof IContextActivation) {
if (value != activation) {
final Collection<IContextActivation> contextActivations = new ArrayList<>(2);
contextActivations.add((IContextActivation) 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<IContextActivation> 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 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<IContextActivation> oldActivations = 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 = registeredWindows.get(null);
if (oldActivations != null) {
final Iterator<IContextActivation> oldActivationItr = oldActivations.iterator();
while (oldActivationItr.hasNext()) {
final IContextActivation activation = 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<IContextActivation> 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() {
@Override
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<IContextActivation> newActivationItr = newActivations.iterator();
while (newActivationItr.hasNext()) {
deactivateContext(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 boolean containsActive(final Collection<IContextActivation> activations) {
final Iterator<IContextActivation> activationItr = activations.iterator();
while (activationItr.hasNext()) {
final IContextActivation activation = 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>.
*/
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<IContextActivation> contextActivations = (Collection<IContextActivation>) 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 = 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<IContextActivation> 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>.
*/
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 int getShellType(final Shell shell) {
// If the shell is null, then return none.
if (shell == null) {
return IContextService.TYPE_NONE;
}
final Collection<IContextActivation> activations = 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<IContextActivation> activationItr = activations.iterator();
while (activationItr.hasNext()) {
final IContextActivation activation = 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 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 StringBuilder buffer = new StringBuilder("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<IContextActivation> 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<IContextActivation> previousActivations = registeredWindows.get(shell);
if (previousActivations != null) {
returnValue = true;
final Iterator<IContextActivation> previousActivationItr = previousActivations.iterator();
while (previousActivationItr.hasNext()) {
final IContextActivation activation = 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() {
@Override
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<IContextActivation> activationItr = activations.iterator();
while (activationItr.hasNext()) {
deactivateContext(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.
*/
@Override
protected 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<IContextActivation> activationsToRecompute = new HashSet<>(ACTIVATIONS_TO_RECOMPUTE_SIZE);
for (int i = 1; i <= 32; i++) {
if ((sourcePriority & (1 << i)) != 0) {
final Collection<IContextActivation> activations = activationsBySourcePriority[i];
if (activations != null) {
final Iterator<IContextActivation> 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<String> changedContextIds = new ArrayList<>(activationsToRecompute.size());
final Iterator<IContextActivation> activationItr = activationsToRecompute.iterator();
while (activationItr.hasNext()) {
final IContextActivation activation = 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<String> changedContextIdItr = changedContextIds.iterator();
while (changedContextIdItr.hasNext()) {
final String contextId = 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<IContextActivation>) 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 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<IContextActivation> previousActivations = registeredWindows.get(shell);
if (previousActivations != null) {
registeredWindows.remove(shell);
final Iterator<IContextActivation> previousActivationItr = previousActivations.iterator();
while (previousActivationItr.hasNext()) {
final IContextActivation activation = 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 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.
*/
@Override
protected 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>
*/
void updateShellKludge() {
updateCurrentState();
sourceChanged(ISources.ACTIVE_SHELL);
}
}