| /******************************************************************************* |
| * Copyright (c) 2005, 2006 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.handlers; |
| |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.SortedSet; |
| import java.util.TreeSet; |
| |
| import org.eclipse.core.commands.Command; |
| import org.eclipse.core.commands.util.Tracing; |
| import org.eclipse.core.expressions.Expression; |
| import org.eclipse.swt.widgets.Shell; |
| import org.eclipse.ui.ISources; |
| import org.eclipse.ui.commands.ICommandService; |
| import org.eclipse.ui.handlers.IHandlerActivation; |
| import org.eclipse.ui.internal.misc.Policy; |
| import org.eclipse.ui.internal.services.EvaluationResultCache; |
| import org.eclipse.ui.internal.services.EvaluationResultCacheComparator; |
| import org.eclipse.ui.internal.services.ExpressionAuthority; |
| |
| /** |
| * <p> |
| * A central authority for resolving conflicts between handlers. This authority |
| * listens to a variety of incoming sources, and updates the underlying commands |
| * if changes in the active handlers occur. |
| * </p> |
| * <p> |
| * This authority encapsulates all of the handler conflict resolution mechanisms |
| * for the workbench. A conflict occurs if two or more handlers are assigned to |
| * the same command identifier. To resolve this conflict, the authority |
| * considers which source the handler came from. |
| * </p> |
| * |
| * @since 3.1 |
| */ |
| final class HandlerAuthority 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_BY_SOURCE_SIZE = 256; |
| |
| /** |
| * 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 = 1024; |
| |
| /** |
| * Whether the workbench command support should kick into debugging mode. |
| * This causes the unresolvable handler conflicts to be printed to the |
| * console. |
| */ |
| private static final boolean DEBUG = Policy.DEBUG_HANDLERS; |
| |
| /** |
| * Whether the performance information should be printed about the |
| * performance of the handler authority. |
| */ |
| private static final boolean DEBUG_PERFORMANCE = Policy.DEBUG_HANDLERS_PERFORMANCE; |
| |
| /** |
| * Whether the workbench command support should kick into verbose debugging |
| * mode. This causes the resolvable handler conflicts to be printed to the |
| * console. |
| */ |
| private static final boolean DEBUG_VERBOSE = Policy.DEBUG_HANDLERS |
| && Policy.DEBUG_HANDLERS_VERBOSE; |
| |
| /** |
| * The command identifier to which the verbose output should be restricted. |
| */ |
| private static final String DEBUG_VERBOSE_COMMAND_ID = Policy.DEBUG_HANDLERS_VERBOSE_COMMAND_ID; |
| |
| /** |
| * The component name to print when displaying tracing information. |
| */ |
| private static final String TRACING_COMPONENT = "HANDLERS"; //$NON-NLS-1$ |
| |
| /** |
| * A bucket sort of the handler activations based on source priority of its |
| * expression. Each expression 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>. |
| * This is an array of {@link Map}, where the maps contain instances of |
| * {@link Collection} containing instances of {@link IHandlerActivation} |
| * indexed by instances of {@link Expression}. |
| */ |
| private final Map[] activationsByExpressionBySourcePriority = new Map[33]; |
| |
| /** |
| * The command service that should be updated when the handlers are |
| * changing. This value is never <code>null</code>. |
| */ |
| private final ICommandService commandService; |
| |
| /** |
| * This is a map of handler activations (<code>SortedSet</code> of |
| * <code>IHandlerActivation</code>) sorted by command identifier (<code>String</code>). |
| * If there is only one handler activation for a command, then the |
| * <code>SortedSet</code> is replaced by a <code>IHandlerActivation</code>. |
| * If there is no activation, the entry should be removed entirely. |
| */ |
| private final Map handlerActivationsByCommandId = new HashMap(); |
| |
| /** |
| * Constructs a new instance of <code>HandlerAuthority</code>. |
| * |
| * @param commandService |
| * The command service from which commands can be retrieved (to |
| * update their handlers); must not be <code>null</code>. |
| */ |
| HandlerAuthority(final ICommandService commandService) { |
| if (commandService == null) { |
| throw new NullPointerException( |
| "The handler authority needs a command service"); //$NON-NLS-1$ |
| } |
| |
| this.commandService = commandService; |
| } |
| |
| /** |
| * Activates a handler on the workbench. This will add it to a master list. |
| * If conflicts exist, they will be resolved based on the source priority. |
| * If conflicts still exist, then no handler becomes active. |
| * |
| * @param activation |
| * The activation; must not be <code>null</code>. |
| */ |
| final void activateHandler(final IHandlerActivation activation) { |
| // First we update the handlerActivationsByCommandId map. |
| final String commandId = activation.getCommandId(); |
| final Object value = handlerActivationsByCommandId.get(commandId); |
| if (value instanceof SortedSet) { |
| final SortedSet handlerActivations = (SortedSet) value; |
| if (!handlerActivations.contains(activation)) { |
| handlerActivations.add(activation); |
| updateCommand(commandId, resolveConflicts(commandId, |
| handlerActivations)); |
| } |
| } else if (value instanceof IHandlerActivation) { |
| if (value != activation) { |
| final SortedSet handlerActivations = new TreeSet( |
| new EvaluationResultCacheComparator()); |
| handlerActivations.add(value); |
| handlerActivations.add(activation); |
| handlerActivationsByCommandId |
| .put(commandId, handlerActivations); |
| updateCommand(commandId, resolveConflicts(commandId, |
| handlerActivations)); |
| } |
| } else { |
| handlerActivationsByCommandId.put(commandId, activation); |
| updateCommand(commandId, (evaluate(activation) ? activation : null)); |
| } |
| |
| // 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) { |
| Map activationsByExpression = activationsByExpressionBySourcePriority[i]; |
| if (activationsByExpression == null) { |
| activationsByExpression = new HashMap( |
| ACTIVATIONS_BY_SOURCE_SIZE); |
| activationsByExpressionBySourcePriority[i] = activationsByExpression; |
| } |
| |
| final Expression expression = activation.getExpression(); |
| Collection activations = (Collection) activationsByExpression |
| .get(expression); |
| if (activations == null) { |
| activations = new HashSet(); |
| activationsByExpression.put(expression, activations); |
| } |
| activations.add(activation); |
| } |
| } |
| } |
| |
| /** |
| * Removes an activation for a handler on the workbench. This will remove it |
| * from the master list, and update the appropriate command, if necessary. |
| * |
| * @param activation |
| * The activation; must not be <code>null</code>. |
| */ |
| final void deactivateHandler(final IHandlerActivation activation) { |
| // First we update the handlerActivationsByCommandId map. |
| final String commandId = activation.getCommandId(); |
| final Object value = handlerActivationsByCommandId.get(commandId); |
| if (value instanceof SortedSet) { |
| final SortedSet handlerActivations = (SortedSet) value; |
| if (handlerActivations.contains(activation)) { |
| handlerActivations.remove(activation); |
| if (handlerActivations.isEmpty()) { |
| handlerActivationsByCommandId.remove(commandId); |
| updateCommand(commandId, null); |
| |
| } else if (handlerActivations.size() == 1) { |
| final IHandlerActivation remainingActivation = (IHandlerActivation) handlerActivations |
| .iterator().next(); |
| handlerActivationsByCommandId.put(commandId, |
| remainingActivation); |
| updateCommand( |
| commandId, |
| (evaluate(remainingActivation) ? remainingActivation |
| : null)); |
| |
| } else { |
| updateCommand(commandId, resolveConflicts(commandId, |
| handlerActivations)); |
| } |
| } |
| } else if (value instanceof IHandlerActivation) { |
| if (value == activation) { |
| handlerActivationsByCommandId.remove(commandId); |
| updateCommand(commandId, null); |
| } |
| } |
| |
| // 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 Map activationsByExpression = activationsByExpressionBySourcePriority[i]; |
| if (activationsByExpression == null) { |
| continue; |
| } |
| |
| final Expression expression = activation.getExpression(); |
| final Collection activations = (Collection) activationsByExpression |
| .get(expression); |
| activations.remove(activation); |
| if (activations.isEmpty()) { |
| activationsByExpression.remove(expression); |
| } |
| |
| if (activationsByExpression.isEmpty()) { |
| activationsByExpressionBySourcePriority[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); |
| } |
| |
| /** |
| * Resolves conflicts between multiple handlers for the same command |
| * identifier. This tries to select the best activation based on the source |
| * priority. For the sake of comparison, activations with the same handler |
| * are considered equivalent (i.e., non-conflicting). |
| * |
| * @param commandId |
| * The identifier of the command for which the conflicts should |
| * be detected; must not be <code>null</code>. This is only |
| * used for debugging purposes. |
| * @param activations |
| * All of the possible handler activations for the given command |
| * identifier; must not be <code>null</code>. |
| * @return The best matching handler activation. If none can be found (e.g., |
| * because of unresolvable conflicts), then this returns |
| * <code>null</code>. |
| */ |
| private final IHandlerActivation resolveConflicts(final String commandId, |
| final SortedSet activations) { |
| // If we don't have any, then there is no match. |
| if (activations.isEmpty()) { |
| return null; |
| } |
| |
| // Cycle over the activations, remembered the current best. |
| final Iterator activationItr = activations.iterator(); |
| IHandlerActivation bestActivation = null; |
| boolean conflict = false; |
| while (activationItr.hasNext()) { |
| final IHandlerActivation currentActivation = (IHandlerActivation) activationItr |
| .next(); |
| if (!evaluate(currentActivation)) { |
| continue; // only consider potentially active handlers |
| } |
| |
| // Check to see if we haven't found a potentially active handler yet |
| if ((DEBUG_VERBOSE) |
| && ((DEBUG_VERBOSE_COMMAND_ID == null) || (DEBUG_VERBOSE_COMMAND_ID |
| .equals(commandId)))) { |
| Tracing.printTrace(TRACING_COMPONENT, " resolveConflicts: eval: " + currentActivation); //$NON-NLS-1$ |
| } |
| if (bestActivation == null) { |
| bestActivation = currentActivation; |
| conflict = false; |
| continue; |
| } |
| |
| // Compare the two handlers. |
| final int comparison = bestActivation.compareTo(currentActivation); |
| if (comparison < 0) { |
| bestActivation = currentActivation; |
| conflict = false; |
| |
| } else if (comparison == 0) { |
| if (currentActivation.getHandler() != bestActivation |
| .getHandler()) { |
| conflict = true; |
| } |
| |
| } else { |
| break; |
| } |
| } |
| |
| // If we are logging information, now is the time to do it. |
| if (DEBUG) { |
| if (conflict) { |
| Tracing.printTrace(TRACING_COMPONENT, |
| "Unresolved conflict detected for '" //$NON-NLS-1$ |
| + commandId + '\''); |
| } else if ((bestActivation != null) |
| && (DEBUG_VERBOSE) |
| && ((DEBUG_VERBOSE_COMMAND_ID == null) || (DEBUG_VERBOSE_COMMAND_ID |
| .equals(commandId)))) { |
| Tracing |
| .printTrace(TRACING_COMPONENT, |
| "Resolved conflict detected. The following activation won: "); //$NON-NLS-1$ |
| Tracing.printTrace(TRACING_COMPONENT, " " + bestActivation); //$NON-NLS-1$ |
| } |
| } |
| |
| // Return the current best. |
| if (conflict) { |
| return null; |
| } |
| return bestActivation; |
| } |
| |
| /** |
| * 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 Collection changedCommandIds = new HashSet( |
| ACTIVATIONS_TO_RECOMPUTE_SIZE); |
| for (int i = 1; i <= 32; i++) { |
| if ((sourcePriority & (1 << i)) != 0) { |
| final Map activationsByExpression = activationsByExpressionBySourcePriority[i]; |
| if (activationsByExpression != null) { |
| final Iterator activationByExpressionItr = activationsByExpression |
| .values().iterator(); |
| while (activationByExpressionItr.hasNext()) { |
| final Collection activations = (Collection) activationByExpressionItr |
| .next(); |
| final Iterator activationItr = activations.iterator(); |
| |
| // Check the first activation to see if it has changed. |
| if (activationItr.hasNext()) { |
| IHandlerActivation activation = (IHandlerActivation) activationItr |
| .next(); |
| final boolean currentActive = evaluate(activation); |
| activation.clearResult(); |
| final boolean newActive = evaluate(activation); |
| if (newActive != currentActive) { |
| changedCommandIds |
| .add(activation.getCommandId()); |
| |
| // Then add every other activation as well. |
| while (activationItr.hasNext()) { |
| activation = (IHandlerActivation) activationItr |
| .next(); |
| // TODO After 3.2, consider making this API. |
| if (activation instanceof EvaluationResultCache) { |
| ((EvaluationResultCache) activation) |
| .setResult(newActive); |
| } else { |
| activation.clearResult(); |
| } |
| changedCommandIds.add(activation |
| .getCommandId()); |
| } |
| } else { |
| while (activationItr.hasNext()) { |
| activation = (IHandlerActivation) activationItr |
| .next(); |
| // if for some reason another activation |
| // doesn't match the new result, update and |
| // mark as changed. It's not as expensive |
| // as it looks :-) |
| if (newActive != evaluate(activation)) { |
| // TODO After 3.2, consider making this |
| // API. |
| if (activation instanceof EvaluationResultCache) { |
| ((EvaluationResultCache) activation) |
| .setResult(newActive); |
| } else { |
| activation.clearResult(); |
| } |
| changedCommandIds.add(activation |
| .getCommandId()); |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| |
| /* |
| * For every command identifier with a changed activation, we resolve |
| * conflicts and trigger an update. |
| */ |
| final Iterator changedCommandIdItr = changedCommandIds.iterator(); |
| while (changedCommandIdItr.hasNext()) { |
| final String commandId = (String) changedCommandIdItr.next(); |
| final Object value = handlerActivationsByCommandId.get(commandId); |
| if (value instanceof IHandlerActivation) { |
| final IHandlerActivation activation = (IHandlerActivation) value; |
| updateCommand(commandId, (evaluate(activation) ? activation |
| : null)); |
| } else if (value instanceof SortedSet) { |
| final IHandlerActivation activation = resolveConflicts( |
| commandId, (SortedSet) value); |
| updateCommand(commandId, activation); |
| } else { |
| updateCommand(commandId, null); |
| } |
| } |
| |
| // If tracing performance, then print the results. |
| if (DEBUG_PERFORMANCE) { |
| final long elapsedTime = System.currentTimeMillis() - startTime; |
| final int size = changedCommandIds.size(); |
| if (size > 0) { |
| Tracing.printTrace(TRACING_COMPONENT, size |
| + " command ids changed in " + elapsedTime + "ms"); //$NON-NLS-1$ //$NON-NLS-2$ |
| } |
| } |
| } |
| |
| /** |
| * Updates the command with the given handler activation. |
| * |
| * @param commandId |
| * The identifier of the command which should be updated; must |
| * not be <code>null</code>. |
| * @param activation |
| * The activation to use; may be <code>null</code> if the |
| * command should have a <code>null</code> handler. |
| */ |
| private final void updateCommand(final String commandId, |
| final IHandlerActivation activation) { |
| final Command command = commandService.getCommand(commandId); |
| if (activation == null) { |
| command.setHandler(null); |
| } else { |
| command.setHandler(activation.getHandler()); |
| } |
| } |
| |
| /** |
| * <p> |
| * Bug 95792. A mechanism by which the key binding architecture can force an |
| * update of the handlers (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); |
| } |
| } |