/*******************************************************************************
 * 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[] 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>.
	 */
	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 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() {

					@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 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 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>.
	 */
	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>.
	 */
	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 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 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 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() {

			@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 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.
	 */
	@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 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 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 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);
	}
}
