/******************************************************************************* * Copyright (c) 2005, 2007 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.io.*; import java.util.*; import org.eclipse.core.commands.Command; import org.eclipse.core.commands.IHandler; import org.eclipse.core.expressions.*; import org.eclipse.core.runtime.*; 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.WorkbenchPlugin; 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 1.0 */ 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(); private Set previousLogs = new HashSet(); /** * 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(); MultiStatus conflicts = new MultiStatus("org.eclipse.ui.workbench", 0, //$NON-NLS-1$ "A handler conflict occurred. This may disable some commands.", //$NON-NLS-1$ null); 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, conflicts)); } } 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, conflicts)); } } else { handlerActivationsByCommandId.put(commandId, activation); updateCommand(commandId, (evaluate(activation) ? activation : null)); } if (conflicts.getSeverity()!=IStatus.OK) { WorkbenchPlugin.log(conflicts); } // 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(); MultiStatus conflicts = new MultiStatus("org.eclipse.ui.workbench", 0, //$NON-NLS-1$ "A handler conflict occurred. This may disable some commands.", //$NON-NLS-1$ null); 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, conflicts)); } } } else if (value instanceof IHandlerActivation) { if (value == activation) { handlerActivationsByCommandId.remove(commandId); updateCommand(commandId, null); } } if (conflicts.getSeverity()!=IStatus.OK) { WorkbenchPlugin.log(conflicts); } // 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, MultiStatus conflicts) { // 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; IHandlerActivation currentActivation = null; boolean conflict = false; while (activationItr.hasNext()) { 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; break; } } 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) { if (previousLogs.add(commandId)) { final StringWriter sw = new StringWriter(); final BufferedWriter buffer = new BufferedWriter(sw); try { buffer.write("Conflict for \'"); //$NON-NLS-1$ buffer.write(commandId); buffer.write("\':"); //$NON-NLS-1$ buffer.newLine(); buffer.write(bestActivation.toString()); buffer.newLine(); buffer.write(currentActivation.toString()); buffer.flush(); } catch (IOException e) { //should never get this. } // TODO: remove 3.2 compatibility hack // IStatus s = new Status(IStatus.WARNING, // "org.eclipse.ui.workbench", //$NON-NLS-1$ // sw.toString()); // conflicts.add(s); } 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(); activation.setResult(newActive); 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)) { activation.setResult(newActive); changedCommandIds.add(activation .getCommandId()); } } } } } } } } MultiStatus conflicts = new MultiStatus("org.eclipse.ui.workbench", 0, //$NON-NLS-1$ "A handler conflict occurred. This may disable some commands.", //$NON-NLS-1$ null); /* * 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, conflicts); updateCommand(commandId, activation); } else { updateCommand(commandId, null); } } if (conflicts.getSeverity()!=IStatus.OK) { WorkbenchPlugin.log(conflicts); } // 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()); commandService.refreshElements(commandId, null); } } /** * <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); } /** * Currently this is a an internal method to help locate a handler. * <p> * DO NOT CALL THIS METHOD. * </p> * * @param commandId * the command id to check * @param context * the context to use for activations * @since 1.0 */ public final IHandler findHandler(String commandId, IEvaluationContext context) { Object o = handlerActivationsByCommandId.get(commandId); if (o instanceof IHandlerActivation) { IHandlerActivation activation = (IHandlerActivation) o; try { if (eval(context, activation)) { return activation.getHandler(); } } catch (CoreException e) { // the evalution failed } } else if (o instanceof SortedSet) { SortedSet activations = (SortedSet) o; IHandlerActivation lastActivation = null; IHandlerActivation currentActivation = null; Iterator i = activations.iterator(); while (i.hasNext() && lastActivation==null) { IHandlerActivation activation = (IHandlerActivation) i.next(); try { if (eval(context, activation)) { lastActivation = currentActivation; currentActivation = activation; } } catch (CoreException e) { // OK, this one is out of the running } } if (currentActivation != null) { if (lastActivation == null) { return currentActivation.getHandler(); } if (lastActivation.getSourcePriority() != currentActivation .getSourcePriority()) { return lastActivation.getHandler(); } } } return null; } /** * Evaluate the expression for the handler and bypass the result cache. * <p> * DO NOT CALL THIS METHOD. * </p> * * @param context * @param activation * @return <code>true</code> if the handler expression can evaluate to * true. * @throws CoreException * @since 1.0 */ private boolean eval(IEvaluationContext context, IHandlerActivation activation) throws CoreException { Expression expression = activation.getExpression(); if (expression == null) { return true; } return expression.evaluate(context) == EvaluationResult.TRUE; } /** * Normally the context returned from getCurrentState() still tracks the * application state. This method creates a copy and fills it in with the * variables that we know about. Currently it does not fill in the active * selection. * <p> * DO NOT CALL THIS METHOD. It is experimental in 3.3. * </p> * * @return an evaluation context with no parent. * @since 1.0 */ public IEvaluationContext getContextSnapshot() { return fillInContext(false); } /** * Normally the context returned from getCurrentState() still tracks the * application state. This method creates a copy and fills it in with all the * variables that we know about. * <p> * DO NOT CALL THIS METHOD. It is experimental in 3.3. * </p> * * @return an evaluation context with no parent. * @since 1.0 */ public IEvaluationContext getFullContextSnapshot() { return fillInContext(true); } private IEvaluationContext fillInContext(boolean fullContext) { IEvaluationContext tmpContext = getCurrentState(); EvaluationContext context = null; if (fullContext) { context = new EvaluationContext(null, tmpContext.getDefaultVariable()); copyVariable(context, tmpContext, ISources.ACTIVE_CURRENT_SELECTION_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_FOCUS_CONTROL_ID_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_FOCUS_CONTROL_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_MENU_EDITOR_INPUT_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_MENU_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_MENU_SELECTION_NAME); } else { context = new EvaluationContext(null, Collections.EMPTY_LIST); } copyVariable(context, tmpContext, ISources.ACTIVE_ACTION_SETS_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_CONTEXT_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_EDITOR_ID_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_EDITOR_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_PART_ID_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_PART_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_SITE_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_WORKBENCH_WINDOW_IS_COOLBAR_VISIBLE_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_WORKBENCH_WINDOW_IS_PERSPECTIVEBAR_VISIBLE_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_WORKBENCH_WINDOW_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_WORKBENCH_WINDOW_SHELL_NAME); copyVariable(context, tmpContext, ISources.ACTIVE_SHELL_NAME); return context; } private void copyVariable(IEvaluationContext context, IEvaluationContext tmpContext, String var) { Object o = tmpContext.getVariable(var); if (o != null) { context.addVariable(var, o); } } }