blob: 32ab07f1e27261547d2b41e13d397ca7417fda00 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2004, 2005 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.jface.bindings;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import org.eclipse.core.commands.CommandManager;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.HandleObjectManager;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.commands.contexts.Context;
import org.eclipse.core.commands.contexts.ContextManager;
import org.eclipse.core.commands.contexts.ContextManagerEvent;
import org.eclipse.core.commands.contexts.IContextManagerListener;
import org.eclipse.core.commands.util.Tracing;
import org.eclipse.jface.bindings.keys.IKeyLookup;
import org.eclipse.jface.bindings.keys.KeyLookupFactory;
import org.eclipse.jface.bindings.keys.KeyStroke;
import org.eclipse.jface.contexts.IContextIds;
import org.eclipse.jface.util.Util;
import org.eclipse.swt.SWT;
/**
* <p>
* A central repository for bindings -- both in the defined and undefined
* states. Schemes and bindings can be created and retrieved using this manager.
* It is possible to listen to changes in the collection of schemes and bindings
* by adding a listener to the manager.
* </p>
* <p>
* The binding manager is very sensitive to performance. Misusing the manager
* can render an application unenjoyable to use. As such, each of the public
* methods states the current run-time performance. In future releases, it is
* guaranteed that the method will run in at least the stated time constraint --
* though it might get faster. Where possible, we have also tried to be memory
* efficient.
* </p>
*
* @since 3.1
*/
public final class BindingManager extends HandleObjectManager implements
IContextManagerListener, ISchemeListener {
/**
* This flag can be set to <code>true</code> if the binding manager should
* print information to <code>System.out</code> when certain boundary
* conditions occur.
*/
public static boolean DEBUG = false;
/**
* The separator character used in locales.
*/
private static final String LOCALE_SEPARATOR = "_"; //$NON-NLS-1$
/**
* </p>
* A utility method for adding entries to a map. The map is checked for
* entries at the key. If such an entry exists, it is expected to be a
* <code>Collection</code>. The value is then appended to the collection.
* If no such entry exists, then a collection is created, and the value
* added to the collection.
* </p>
*
* @param map
* The map to modify; if this value is <code>null</code>, then
* this method simply returns.
* @param key
* The key to look up in the map; may be <code>null</code>.
* @param value
* The value to look up in the map; may be <code>null</code>.
*/
private static final void addReverseLookup(final Map map, final Object key,
final Object value) {
if (map == null) {
return;
}
final Object currentValue = map.get(key);
if (currentValue != null) {
final Collection values = (Collection) currentValue;
values.add(value);
} else { // currentValue == null
final Collection values = new ArrayList(1);
values.add(value);
map.put(key, values);
}
}
/**
* <p>
* Takes a fully-specified string, and converts it into an array of
* increasingly less-specific strings. So, for example, "en_GB" would become
* ["en_GB", "en", "", null].
* </p>
* <p>
* This method runs in linear time (O(n)) over the length of the string.
* </p>
*
* @param string
* The string to break apart into its less specific components;
* should not be <code>null</code>.
* @param separator
* The separator that indicates a separation between a degrees of
* specificity; should not be <code>null</code>.
* @return An array of strings from the most specific (i.e.,
* <code>string</code>) to the least specific (i.e.,
* <code>null</code>).
*/
private static final String[] expand(String string, final String separator) {
// Test for boundary conditions.
if (string == null || separator == null) {
return new String[0];
}
final List strings = new ArrayList();
final StringBuffer stringBuffer = new StringBuffer();
string = string.trim(); // remove whitespace
if (string.length() > 0) {
final StringTokenizer stringTokenizer = new StringTokenizer(string,
separator);
while (stringTokenizer.hasMoreElements()) {
if (stringBuffer.length() > 0)
stringBuffer.append(separator);
stringBuffer.append(((String) stringTokenizer.nextElement())
.trim());
strings.add(stringBuffer.toString());
}
}
Collections.reverse(strings);
strings.add(Util.ZERO_LENGTH_STRING);
strings.add(null);
return (String[]) strings.toArray(new String[strings.size()]);
}
/**
* The active bindings. This is a map of triggers (
* <code>TriggerSequence</code>) to bindings (<code>Binding</code>).
* This value will only be <code>null</code> if the active bindings have
* not yet been computed. Otherwise, this value may be empty.
*/
private Map activeBindings = null;
/**
* The active bindings indexed by fully-parameterized commands. This is a
* map of fully-parameterized commands (<code>ParameterizedCommand</code>)
* to triggers ( <code>TriggerSequence</code>). This value will only be
* <code>null</code> if the active bindings have not yet been computed.
* Otherwise, this value may be empty.
*/
private Map activeBindingsByParameterizedCommand = null;
/**
* The scheme that is currently active. An active scheme is the one that is
* currently dictating which bindings will actually work. This value may be
* <code>null</code> if there is no active scheme. If the active scheme
* becomes undefined, then this should automatically revert to
* <code>null</code>.
*/
private Scheme activeScheme = null;
/**
* The array of scheme identifiers, starting with the active scheme and
* moving up through its parents. This value may be <code>null</code> if
* there is no active scheme.
*/
private String[] activeSchemeIds = null;
/**
* The number of bindings in the <code>bindings</code> array.
*/
private int bindingCount = 0;
/**
* The array of all bindings currently handled by this manager. This array
* is the raw list of bindings, as provided to this manager. This value may
* be <code>null</code> if there are no bindings. The size of this array
* is not necessarily the number of bindings.
*/
private Binding[] bindings = null;
/**
* A cache of the bindings previously computed by this manager. This value
* may be empty, but it is never <code>null</code>. This is a map of
* <code>CachedBindingSet</code> to <code>CachedBindingSet</code>.
*/
private Map cachedBindings = new HashMap();
/**
* The command manager for this binding manager. This manager is only needed
* for the <code>getActiveBindingsFor(String)</code> method. This value is
* guaranteed to never be <code>null</code>.
*/
private final CommandManager commandManager;
/**
* The context manager for this binding manager. For a binding manager to
* function, it needs to listen for changes to the contexts. This value is
* guaranteed to never be <code>null</code>.
*/
private final ContextManager contextManager;
/**
* The locale for this manager. This defaults to the current locale. The
* value will never be <code>null</code>.
*/
private String locale = Locale.getDefault().toString();
/**
* The array of locales, starting with the active locale and moving up
* through less specific representations of the locale. For example,
* ["en_US", "en", "", null]. This value will never be <code>null</code>.
*/
private String[] locales = expand(locale, LOCALE_SEPARATOR);
/**
* The platform for this manager. This defaults to the current platform. The
* value will never be <code>null</code>.
*/
private String platform = SWT.getPlatform();
/**
* The array of platforms, starting with the active platform and moving up
* through less specific representations of the platform. For example,
* ["gtk", "", null]. This value will never be <code>null,/code>.
*/
private String[] platforms = expand(platform, Util.ZERO_LENGTH_STRING);
/**
* A map of prefixes (<code>TriggerSequence</code>) to a map of
* available completions (possibly <code>null</code>, which means there
* is an exact match). The available completions is a map of trigger (<code>TriggerSequence</code>)
* to bindings (<code>Binding</code>). This value may be
* <code>null</code> if there is no existing solution.
*/
private Map prefixTable = null;
/**
* <p>
* Constructs a new instance of <code>BindingManager</code>.
* </p>
* <p>
* This method completes in amortized constant time (O(1)).
* </p>
*
* @param contextManager
* The context manager that will support this binding manager.
* This value must not be <code>null</code>.
* @param commandManager
* The command manager that will support this binding manager.
* This value must not be <code>null</code>.
*/
public BindingManager(final ContextManager contextManager,
final CommandManager commandManager) {
if (contextManager == null) {
throw new NullPointerException(
"A binding manager requires a context manager"); //$NON-NLS-1$
}
if (commandManager == null) {
throw new NullPointerException(
"A binding manager requires a command manager"); //$NON-NLS-1$
}
this.contextManager = contextManager;
contextManager.addContextManagerListener(this);
this.commandManager = commandManager;
}
/**
* <p>
* Adds a single new binding to the existing array of bindings. If the array
* is currently <code>null</code>, then a new array is created and this
* binding is added to it. This method does not detect duplicates.
* </p>
* <p>
* This method completes in amortized <code>O(1)</code>.
* </p>
*
* @param binding
* The binding to be added; must not be <code>null</code>.
*/
public final void addBinding(final Binding binding) {
if (binding == null) {
throw new NullPointerException("Cannot add a null binding"); //$NON-NLS-1$
}
if (bindings == null) {
bindings = new Binding[1];
} else if (bindingCount >= bindings.length) {
final Binding[] oldBindings = bindings;
bindings = new Binding[oldBindings.length * 2];
System.arraycopy(oldBindings, 0, bindings, 0, oldBindings.length);
}
bindings[bindingCount++] = binding;
clearCache();
}
/**
* <p>
* Adds a listener to this binding manager. The listener will be notified
* when the set of defined schemes or bindings changes. This can be used to
* track the global appearance and disappearance of bindings.
* </p>
* <p>
* This method completes in amortized constant time (<code>O(1)</code>).
* </p>
*
* @param listener
* The listener to attach; must not be <code>null</code>.
*/
public final void addBindingManagerListener(
final IBindingManagerListener listener) {
addListenerObject(listener);
}
/**
* <p>
* Builds a prefix table look-up for a map of active bindings.
* </p>
* <p>
* This method takes <code>O(mn)</code>, where <code>m</code> is the
* length of the trigger sequences and <code>n</code> is the number of
* bindings.
* </p>
*
* @param activeBindings
* The map of triggers (<code>TriggerSequence</code>) to
* command ids (<code>String</code>) which are currently
* active. This value may be <code>null</code> if there are no
* active bindings, and it may be empty. It must not be
* <code>null</code>.
* @return A map of prefixes (<code>TriggerSequence</code>) to a map of
* available completions (possibly <code>null</code>, which means
* there is an exact match). The available completions is a map of
* trigger (<code>TriggerSequence</code>) to command identifier (<code>String</code>).
* This value will never be <code>null</code>, but may be empty.
*/
private final Map buildPrefixTable(final Map activeBindings) {
final Map prefixTable = new HashMap();
final Iterator bindingItr = activeBindings.entrySet().iterator();
while (bindingItr.hasNext()) {
final Map.Entry entry = (Map.Entry) bindingItr.next();
final TriggerSequence triggerSequence = (TriggerSequence) entry
.getKey();
// Add the perfect match.
if (!prefixTable.containsKey(triggerSequence)) {
prefixTable.put(triggerSequence, null);
}
final TriggerSequence[] prefixes = triggerSequence.getPrefixes();
final int prefixesLength = prefixes.length;
if (prefixesLength == 0) {
continue;
}
// Break apart the trigger sequence.
final Binding binding = (Binding) entry.getValue();
for (int i = 0; i < prefixesLength; i++) {
final TriggerSequence prefix = prefixes[i];
final Object value = prefixTable.get(prefix);
if ((prefixTable.containsKey(prefix)) && (value instanceof Map)) {
((Map) value).put(triggerSequence, binding);
} else {
final Map map = new HashMap();
prefixTable.put(prefix, map);
map.put(triggerSequence, binding);
}
}
}
return prefixTable;
}
/**
* <p>
* Clears the cache, and the existing solution. If debugging is turned on,
* then this will also print a message to standard out.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
* </p>
*/
private final void clearCache() {
if (DEBUG) {
Tracing.printTrace("BINDINGS", "Clearing cache"); //$NON-NLS-1$ //$NON-NLS-2$
}
cachedBindings.clear();
clearSolution();
}
/**
* <p>
* Clears the existing solution.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
*/
private final void clearSolution() {
setActiveBindings(null, null, null);
}
/**
* Compares the identifier of two schemes, and decides which scheme is the
* youngest (i.e., the child) of the two. Both schemes should be active
* schemes.
*
* @param schemeId1
* The identifier of the first scheme; must not be
* <code>null</code>.
* @param schemeId2
* The identifier of the second scheme; must not be
* <code>null</code>.
* @return <code>0</code> if the two schemes are equal of if neither
* scheme is active; <code>1</code> if the second scheme is the
* youngest; and <code>-1</code> if the first scheme is the
* youngest.
* @since 3.2
*/
private final int compareSchemes(final String schemeId1,
final String schemeId2) {
if (!schemeId2.equals(schemeId1)) {
for (int i = 0; i < activeSchemeIds.length; i++) {
final String schemePointer = activeSchemeIds[i];
if (schemeId2.equals(schemePointer)) {
return 1;
} else if (schemeId1.equals(schemePointer)) {
return -1;
}
}
}
return 0;
}
/**
* <p>
* Computes the bindings given the context tree, and inserts them into the
* <code>commandIdsByTrigger</code>. It is assumed that
* <code>locales</code>,<code>platforsm</code> and
* <code>schemeIds</code> correctly reflect the state of the application.
* This method does not deal with caching.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of bindings.
* </p>
*
* @param activeContextTree
* The map representing the tree of active contexts. The map is
* one of child to parent, each being a context id (
* <code>String</code>). The keys are never <code>null</code>,
* but the values may be (i.e., no parent). This map may be
* empty. It may be <code>null</code> if we shouldn't consider
* contexts.
* @param bindingsByTrigger
* The empty of map that is intended to be filled with triggers (
* <code>TriggerSequence</code>) to bindings (
* <code>Binding</code>). This value must not be
* <code>null</code> and must be empty.
* @param triggersByCommandId
* The empty of map that is intended to be filled with command
* identifiers (<code>String</code>) to triggers (
* <code>TriggerSequence</code>). This value must either be
* <code>null</code> (indicating that these values are not
* needed), or empty (indicating that this map should be
* computed).
*/
private final void computeBindings(final Map activeContextTree,
final Map bindingsByTrigger, final Map triggersByCommandId) {
/*
* FIRST PASS: Remove all of the bindings that are marking deletions.
*/
final Binding[] trimmedBindings = removeDeletions(bindings);
/*
* SECOND PASS: Just throw in bindings that match the current state. If
* there is more than one match for a binding, then create a list.
*/
final Map possibleBindings = new HashMap();
final int length = trimmedBindings.length;
for (int i = 0; i < length; i++) {
final Binding binding = trimmedBindings[i];
boolean found;
// Check the context.
final String contextId = binding.getContextId();
if ((activeContextTree != null)
&& (!activeContextTree.containsKey(contextId))) {
continue;
}
// Check the locale.
if (!localeMatches(binding)) {
continue;
}
// Check the platform.
if (!platformMatches(binding)) {
continue;
}
// Check the scheme ids.
final String schemeId = binding.getSchemeId();
found = false;
if (activeSchemeIds != null) {
for (int j = 0; j < activeSchemeIds.length; j++) {
if (Util.equals(schemeId, activeSchemeIds[j])) {
found = true;
break;
}
}
}
if (!found) {
continue;
}
// Insert the match into the list of possible matches.
final TriggerSequence trigger = binding.getTriggerSequence();
final Object existingMatch = possibleBindings.get(trigger);
if (existingMatch instanceof Binding) {
possibleBindings.remove(trigger);
final Collection matches = new ArrayList();
matches.add(existingMatch);
matches.add(binding);
possibleBindings.put(trigger, matches);
} else if (existingMatch instanceof Collection) {
final Collection matches = (Collection) existingMatch;
matches.add(binding);
} else {
possibleBindings.put(trigger, binding);
}
}
/*
* THIRD PASS: In this pass, we move any non-conflicting bindings
* directly into the map. In the case of conflicts, we apply some
* further logic to try to resolve them. If the conflict can't be
* resolved, then we log the problem.
*/
final Iterator possibleBindingItr = possibleBindings.entrySet()
.iterator();
while (possibleBindingItr.hasNext()) {
final Map.Entry entry = (Map.Entry) possibleBindingItr.next();
final TriggerSequence trigger = (TriggerSequence) entry.getKey();
final Object match = entry.getValue();
/*
* What we do depends slightly on whether we are trying to build a
* list of all possible bindings (disregarding context), or a flat
* map given the currently active contexts.
*/
if (activeContextTree == null) {
// We are building the list of all possible bindings.
final Collection bindings = new ArrayList();
if (match instanceof Binding) {
bindings.add(match);
bindingsByTrigger.put(trigger, bindings);
addReverseLookup(triggersByCommandId, ((Binding) match)
.getParameterizedCommand(), trigger);
} else if (match instanceof Collection) {
bindings.addAll(resolveConflicts((Collection) match));
bindingsByTrigger.put(trigger, bindings);
final Iterator matchItr = bindings.iterator();
while (matchItr.hasNext()) {
addReverseLookup(triggersByCommandId,
((Binding) matchItr.next())
.getParameterizedCommand(), trigger);
}
}
} else {
// We are building the flat map of trigger to commands.
if (match instanceof Binding) {
final Binding binding = (Binding) match;
bindingsByTrigger.put(trigger, binding);
addReverseLookup(triggersByCommandId, binding
.getParameterizedCommand(), trigger);
} else if (match instanceof Collection) {
final Binding winner = resolveConflicts((Collection) match,
activeContextTree);
if (winner == null) {
if (DEBUG) {
Tracing.printTrace("BINDINGS", //$NON-NLS-1$
"A conflict occurred for " + trigger); //$NON-NLS-1$
Tracing.printTrace("BINDINGS", " " + match); //$NON-NLS-1$ //$NON-NLS-2$
}
} else {
bindingsByTrigger.put(trigger, winner);
addReverseLookup(triggersByCommandId, winner
.getParameterizedCommand(), trigger);
}
}
}
}
}
/**
* <p>
* Notifies this manager that the context manager has changed. This method
* is intended for internal use only.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
* </p>
*/
public final void contextManagerChanged(
final ContextManagerEvent contextManagerEvent) {
if (contextManagerEvent.isActiveContextsChanged()) {
clearSolution();
}
}
/**
* Returns the number of strokes in an array of triggers. It is assumed that
* there is one natural key per trigger. The strokes are counted based on
* the type of key. Natural keys are worth one; ctrl is worth two; shift is
* worth four; and alt is worth eight.
*
* @param triggers
* The triggers on which to count strokes; must not be
* <code>null</code>.
* @return The value of the strokes in the triggers.
* @since 3.2
*/
private final int countStrokes(final Trigger[] triggers) {
int strokeCount = triggers.length;
for (int i = 0; i < triggers.length; i++) {
final Trigger trigger = triggers[i];
if (trigger instanceof KeyStroke) {
final KeyStroke keyStroke = (KeyStroke) trigger;
final int modifierKeys = keyStroke.getModifierKeys();
final IKeyLookup lookup = KeyLookupFactory.getDefault();
if ((modifierKeys & lookup.getAlt()) != 0) {
strokeCount += 8;
}
if ((modifierKeys & lookup.getCtrl()) != 0) {
strokeCount += 2;
}
if ((modifierKeys & lookup.getShift()) != 0) {
strokeCount += 4;
}
if ((modifierKeys & lookup.getCommand()) != 0) {
strokeCount += 2;
}
} else {
strokeCount += 99;
}
}
return strokeCount;
}
/**
* <p>
* Creates a tree of context identifiers, representing the hierarchical
* structure of the given contexts. The tree is structured as a mapping from
* child to parent.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the height of the context tree.
* </p>
*
* @param contextIds
* The set of context identifiers to be converted into a tree;
* must not be <code>null</code>.
* @return The tree of contexts to use; may be empty, but never
* <code>null</code>. The keys and values are both strings.
*/
private final Map createContextTreeFor(final Set contextIds) {
final Map contextTree = new HashMap();
final Iterator contextIdItr = contextIds.iterator();
while (contextIdItr.hasNext()) {
String childContextId = (String) contextIdItr.next();
while (childContextId != null) {
// Check if we've already got the part of the tree from here up.
if (contextTree.containsKey(childContextId)) {
break;
}
// Retrieve the context.
final Context childContext = contextManager
.getContext(childContextId);
// Add the child-parent pair to the tree.
try {
final String parentContextId = childContext.getParentId();
contextTree.put(childContextId, parentContextId);
childContextId = parentContextId;
} catch (final NotDefinedException e) {
break; // stop ascending
}
}
}
return contextTree;
}
/**
* <p>
* Creates a tree of context identifiers, representing the hierarchical
* structure of the given contexts. The tree is structured as a mapping from
* child to parent. In this tree, the key binding specific filtering of
* contexts will have taken place.
* </p>
* <p>
* This method completes in <code>O(n^2)</code>, where <code>n</code>
* is the height of the context tree.
* </p>
*
* @param contextIds
* The set of context identifiers to be converted into a tree;
* must not be <code>null</code>.
* @return The tree of contexts to use; may be empty, but never
* <code>null</code>. The keys and values are both strings.
*/
private final Map createFilteredContextTreeFor(final Set contextIds) {
// Check to see whether a dialog or window is active.
boolean dialog = false;
boolean window = false;
Iterator contextIdItr = contextIds.iterator();
while (contextIdItr.hasNext()) {
final String contextId = (String) contextIdItr.next();
if (IContextIds.CONTEXT_ID_DIALOG.equals(contextId)) {
dialog = true;
continue;
}
if (IContextIds.CONTEXT_ID_WINDOW.equals(contextId)) {
window = true;
continue;
}
}
/*
* Remove all context identifiers for contexts whose parents are dialog
* or window, and the corresponding dialog or window context is not
* active.
*/
try {
contextIdItr = contextIds.iterator();
while (contextIdItr.hasNext()) {
String contextId = (String) contextIdItr.next();
Context context = contextManager.getContext(contextId);
String parentId = context.getParentId();
while (parentId != null) {
if (IContextIds.CONTEXT_ID_DIALOG.equals(parentId)) {
if (!dialog) {
contextIdItr.remove();
}
break;
}
if (IContextIds.CONTEXT_ID_WINDOW.equals(parentId)) {
if (!window) {
contextIdItr.remove();
}
break;
}
if (IContextIds.CONTEXT_ID_DIALOG_AND_WINDOW
.equals(parentId)) {
if ((!window) && (!dialog)) {
contextIdItr.remove();
}
break;
}
context = contextManager.getContext(parentId);
parentId = context.getParentId();
}
}
} catch (NotDefinedException e) {
if (DEBUG) {
Tracing.printTrace("BINDINGS", "NotDefinedException('" //$NON-NLS-1$ //$NON-NLS-2$
+ e.getMessage()
+ "') while filtering dialog/window contexts"); //$NON-NLS-1$
}
}
return createContextTreeFor(contextIds);
}
/**
* <p>
* Notifies all of the listeners to this manager that the defined or active
* schemes of bindings have changed.
* </p>
* <p>
* The time this method takes to complete is dependent on external
* listeners.
* </p>
*
* @param event
* The event to send to all of the listeners; must not be
* <code>null</code>.
*/
private final void fireBindingManagerChanged(final BindingManagerEvent event) {
if (event == null)
throw new NullPointerException();
final Object[] listeners = getListeners();
for (int i = 0; i < listeners.length; i++) {
final IBindingManagerListener listener = (IBindingManagerListener) listeners[i];
listener.bindingManagerChanged(event);
}
}
/**
* <p>
* Returns the active bindings.
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the active bindings are
* not yet computed, then this completes in <code>O(nn)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @return The map of triggers (<code>TriggerSequence</code>) to
* bindings (<code>Binding</code>) which are currently active.
* This value may be <code>null</code> if there are no active
* bindings, and it may be empty.
*/
private final Map getActiveBindings() {
if (activeBindings == null) {
recomputeBindings();
}
return Collections.unmodifiableMap(activeBindings);
}
/**
* <p>
* Returns the active bindings indexed by command identifier.
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the active bindings are
* not yet computed, then this completes in <code>O(nn)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @return The map of fully-parameterized commands (<code>ParameterizedCommand</code>)
* to triggers (<code>TriggerSequence</code>) which are
* currently active. This value may be <code>null</code> if there
* are no active bindings, and it may be empty.
*/
private final Map getActiveBindingsByParameterizedCommand() {
if (activeBindingsByParameterizedCommand == null) {
recomputeBindings();
}
return Collections
.unmodifiableMap(activeBindingsByParameterizedCommand);
}
/**
* <p>
* Computes the bindings for the current state of the application, but
* disregarding the current contexts. This can be useful when trying to
* display all the possible bindings.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of bindings.
* </p>
*
* @return A map of trigger (<code>TriggerSequence</code>) to bindings (
* <code>Collection</code> containing <code>Binding</code>).
* This map may be empty, but it is never <code>null</code>.
*/
public final Map getActiveBindingsDisregardingContext() {
if (bindings == null) {
// Not yet initialized. This is happening too early. Do nothing.
return Collections.EMPTY_MAP;
}
// Build a cached binding set for that state.
final CachedBindingSet bindingCache = new CachedBindingSet(null,
locales, platforms, activeSchemeIds);
/*
* Check if the cached binding set already exists. If so, simply set the
* active bindings and return.
*/
CachedBindingSet existingCache = (CachedBindingSet) cachedBindings
.get(bindingCache);
if (existingCache == null) {
existingCache = bindingCache;
cachedBindings.put(existingCache, existingCache);
}
Map commandIdsByTrigger = existingCache.getBindingsByTrigger();
if (commandIdsByTrigger != null) {
if (DEBUG) {
Tracing.printTrace("BINDINGS", "Cache hit"); //$NON-NLS-1$ //$NON-NLS-2$
}
return Collections.unmodifiableMap(commandIdsByTrigger);
}
// There is no cached entry for this.
if (DEBUG) {
Tracing.printTrace("BINDINGS", "Cache miss"); //$NON-NLS-1$ //$NON-NLS-2$
}
// Compute the active bindings.
commandIdsByTrigger = new HashMap();
final Map triggersByParameterizedCommand = new HashMap();
computeBindings(null, commandIdsByTrigger,
triggersByParameterizedCommand);
existingCache.setBindingsByTrigger(commandIdsByTrigger);
existingCache.setTriggersByCommandId(triggersByParameterizedCommand);
return Collections.unmodifiableMap(commandIdsByTrigger);
}
/**
* <p>
* Computes the bindings for the current state of the application, but
* disregarding the current contexts. This can be useful when trying to
* display all the possible bindings.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of bindings.
* </p>
*
* @return A map of trigger (<code>TriggerSequence</code>) to bindings (
* <code>Collection</code> containing <code>Binding</code>).
* This map may be empty, but it is never <code>null</code>.
* @since 3.2
*/
private final Map getActiveBindingsDisregardingContextByParameterizedCommand() {
if (bindings == null) {
// Not yet initialized. This is happening too early. Do nothing.
return Collections.EMPTY_MAP;
}
// Build a cached binding set for that state.
final CachedBindingSet bindingCache = new CachedBindingSet(null,
locales, platforms, activeSchemeIds);
/*
* Check if the cached binding set already exists. If so, simply set the
* active bindings and return.
*/
CachedBindingSet existingCache = (CachedBindingSet) cachedBindings
.get(bindingCache);
if (existingCache == null) {
existingCache = bindingCache;
cachedBindings.put(existingCache, existingCache);
}
Map triggersByParameterizedCommand = existingCache
.getTriggersByCommandId();
if (triggersByParameterizedCommand != null) {
if (DEBUG) {
Tracing.printTrace("BINDINGS", "Cache hit"); //$NON-NLS-1$ //$NON-NLS-2$
}
return Collections.unmodifiableMap(triggersByParameterizedCommand);
}
// There is no cached entry for this.
if (DEBUG) {
Tracing.printTrace("BINDINGS", "Cache miss"); //$NON-NLS-1$ //$NON-NLS-2$
}
// Compute the active bindings.
final Map commandIdsByTrigger = new HashMap();
triggersByParameterizedCommand = new HashMap();
computeBindings(null, commandIdsByTrigger,
triggersByParameterizedCommand);
existingCache.setBindingsByTrigger(commandIdsByTrigger);
existingCache.setTriggersByCommandId(triggersByParameterizedCommand);
System.out.println(triggersByParameterizedCommand);
return Collections.unmodifiableMap(triggersByParameterizedCommand);
}
/**
* <p>
* Computes the bindings for the current state of the application, but
* disregarding the current contexts. This can be useful when trying to
* display all the possible bindings.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of bindings.
* </p>
*
* @return All of the active bindings (<code>Binding</code>), not sorted
* in any fashion. This collection may be empty, but it is never
* <code>null</code>.
*/
public final Collection getActiveBindingsDisregardingContextFlat() {
final Collection bindingCollections = getActiveBindingsDisregardingContext()
.values();
final Collection mergedBindings = new ArrayList();
final Iterator bindingCollectionItr = bindingCollections.iterator();
while (bindingCollectionItr.hasNext()) {
final Collection bindingCollection = (Collection) bindingCollectionItr
.next();
if ((bindingCollection != null) && (!bindingCollection.isEmpty())) {
mergedBindings.addAll(bindingCollection);
}
}
return mergedBindings;
}
/**
* <p>
* Returns the active bindings for a particular command identifier, but
* discounting the current contexts. This method operates in O(n) time over
* the number of bindings.
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the active bindings are
* not yet computed, then this completes in <code>O(nn)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @param parameterizedCommand
* The fully-parameterized command whose bindings are requested.
* This argument may be <code>null</code>.
* @return The array of active triggers (<code>TriggerSequence</code>)
* for a particular command identifier. This value is guaranteed to
* never be <code>null</code>, but it may be empty.
* @since 3.2
*/
public final TriggerSequence[] getActiveBindingsDisregardingContextFor(
final ParameterizedCommand parameterizedCommand) {
final Object object = getActiveBindingsDisregardingContextByParameterizedCommand()
.get(parameterizedCommand);
if (object instanceof Collection) {
final Collection collection = (Collection) object;
return (TriggerSequence[]) collection
.toArray(new TriggerSequence[collection.size()]);
}
return new TriggerSequence[0];
}
/**
* <p>
* Returns the active bindings for a particular command identifier. This
* method operates in O(n) time over the number of bindings.
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the active bindings are
* not yet computed, then this completes in <code>O(nn)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @param parameterizedCommand
* The fully-parameterized command whose bindings are requested.
* This argument may be <code>null</code>.
* @return The array of active triggers (<code>TriggerSequence</code>)
* for a particular command identifier. This value is guaranteed to
* never be <code>null</code>, but it may be empty.
*/
public final TriggerSequence[] getActiveBindingsFor(
final ParameterizedCommand parameterizedCommand) {
final Object object = getActiveBindingsByParameterizedCommand().get(
parameterizedCommand);
if (object instanceof Collection) {
final Collection collection = (Collection) object;
return (TriggerSequence[]) collection
.toArray(new TriggerSequence[collection.size()]);
}
return new TriggerSequence[0];
}
/**
* <p>
* Returns the active bindings for a particular command identifier. This
* method operates in O(n) time over the number of bindings.
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the active bindings are
* not yet computed, then this completes in <code>O(nn)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @param commandId
* The identifier of the command whose bindings are requested.
* This argument may be <code>null</code>. It is assumed that
* the command has no parameters.
* @return The array of active triggers (<code>TriggerSequence</code>)
* for a particular command identifier. This value is guaranteed not
* to be <code>null</code>, but it may be empty.
*/
public final TriggerSequence[] getActiveBindingsFor(final String commandId) {
final ParameterizedCommand parameterizedCommand = new ParameterizedCommand(
commandManager.getCommand(commandId), null);
final Object object = getActiveBindingsByParameterizedCommand().get(
parameterizedCommand);
if (object instanceof Collection) {
final Collection collection = (Collection) object;
return (TriggerSequence[]) collection
.toArray(new TriggerSequence[collection.size()]);
}
return new TriggerSequence[0];
}
/**
* A variation on {@link BindingManager#getActiveBindingsFor(String)} that
* returns an array of bindings, rather than trigger sequences. This method
* is needed for doing "best" calculationgs on the active bindings.
*
* @param commandId
* The identifier of the command for which the active bindings
* should be retrieved; must not be <code>null</code>.
* @return The active bindings for the given command; this value may be
* <code>null</code> if there are no active bindings.
* @since 3.2
*/
private final Binding[] getActiveBindingsFor1(final String commandId) {
final TriggerSequence[] triggers = getActiveBindingsFor(commandId);
if (triggers.length == 0) {
return null;
}
final Map activeBindings = getActiveBindings();
if (activeBindings != null) {
final Binding[] bindings = new Binding[triggers.length];
for (int i = 0; i < triggers.length; i++) {
final TriggerSequence triggerSequence = triggers[i];
final Object object = activeBindings.get(triggerSequence);
final Binding binding = (Binding) object;
bindings[i] = binding;
}
return bindings;
}
return null;
}
/**
* <p>
* Gets the currently active scheme.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
* </p>
*
* @return The active scheme; may be <code>null</code> if there is no
* active scheme. If a scheme is returned, it is guaranteed to be
* defined.
*/
public final Scheme getActiveScheme() {
return activeScheme;
}
/**
* Gets the best active binding for a command. The best binding is the one
* that would be most appropriate to show in a menu. Bindings which belong
* to a child scheme are given preference over those in a parent scheme. The
* rest of the calculaton is based most on various concepts of "length", as
* well as giving some modifier keys preference (e.g., <code>Alt</code> is
* less likely to appear than <code>Ctrl</code>).
*
* @param commandId
* The identifier of the command for which the best active
* binding should be retrieved; must not be <code>null</code>.
* @return The trigger sequence for the best binding; may be
* <code>null</code> if no bindings are active for the given
* command.
* @since 3.2
*/
public final TriggerSequence getBestActiveBindingFor(final String commandId) {
final Binding[] bindings = getActiveBindingsFor1(commandId);
if ((bindings == null) || (bindings.length == 0)) {
return null;
}
Binding bestBinding = bindings[0];
int compareTo;
for (int i = 1; i < bindings.length; i++) {
final Binding currentBinding = bindings[i];
// Bindings in a child scheme are always given preference.
final String bestSchemeId = bestBinding.getSchemeId();
final String currentSchemeId = currentBinding.getSchemeId();
compareTo = compareSchemes(bestSchemeId, currentSchemeId);
if (compareTo > 0) {
bestBinding = currentBinding;
}
if (compareTo != 0) {
continue;
}
/*
* Check to see which has the least number of triggers in the
* trigger sequence.
*/
final TriggerSequence bestTriggerSequence = bestBinding
.getTriggerSequence();
final TriggerSequence currentTriggerSequence = currentBinding
.getTriggerSequence();
final Trigger[] bestTriggers = bestTriggerSequence.getTriggers();
final Trigger[] currentTriggers = currentTriggerSequence
.getTriggers();
compareTo = bestTriggers.length - currentTriggers.length;
if (compareTo > 0) {
bestBinding = currentBinding;
}
if (compareTo != 0) {
continue;
}
/*
* Compare the number of keys pressed in each trigger sequence. Some
* types of keys count less than others (i.e., some types of
* modifiers keys are less likely to be chosen).
*/
compareTo = countStrokes(bestTriggers)
- countStrokes(currentTriggers);
if (compareTo > 0) {
bestBinding = currentBinding;
}
if (compareTo != 0) {
continue;
}
// If this is still a tie, then just chose the shortest text.
compareTo = bestTriggerSequence.format().length()
- currentTriggerSequence.format().length();
if (compareTo > 0) {
bestBinding = currentBinding;
}
}
return bestBinding.getTriggerSequence();
}
/**
* <p>
* Returns the set of all bindings managed by this class.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
* </p>
*
* @return The array of all bindings. This value may be <code>null</code>
* and it may be empty.
*/
public final Binding[] getBindings() {
if (bindings == null) {
return null;
}
final Binding[] returnValue = new Binding[bindingCount];
System.arraycopy(bindings, 0, returnValue, 0, bindingCount);
return returnValue;
}
/**
* <p>
* Returns the array of schemes that are defined.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
* </p>
*
* @return The array of defined schemes; this value may be empty or
* <code>null</code>.
*/
public final Scheme[] getDefinedSchemes() {
return (Scheme[]) definedHandleObjects
.toArray(new Scheme[definedHandleObjects.size()]);
}
/**
* <p>
* Returns the active locale for this binding manager. The locale is in the
* same format as <code>Locale.getDefault().toString()</code>.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
* </p>
*
* @return The active locale; never <code>null</code>.
*/
public final String getLocale() {
return locale;
}
/**
* <p>
* Returns all of the possible bindings that start with the given trigger
* (but are not equal to the given trigger).
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the bindings aren't
* currently computed, then this completes in <code>O(n)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @param trigger
* The prefix to look for; must not be <code>null</code>.
* @return A map of triggers (<code>TriggerSequence</code>) to bindings (<code>Binding</code>).
* This map may be empty, but it is never <code>null</code>.
*/
public final Map getPartialMatches(final TriggerSequence trigger) {
final Map partialMatches = (Map) getPrefixTable().get(trigger);
if (partialMatches == null) {
return Collections.EMPTY_MAP;
}
return partialMatches;
}
/**
* <p>
* Returns the command identifier for the active binding matching this
* trigger, if any.
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the bindings aren't
* currently computed, then this completes in <code>O(n)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @param trigger
* The trigger to match; may be <code>null</code>.
* @return The binding that matches, if any; <code>null</code> otherwise.
*/
public final Binding getPerfectMatch(final TriggerSequence trigger) {
return (Binding) getActiveBindings().get(trigger);
}
/**
* <p>
* Returns the active platform for this binding manager. The platform is in
* the same format as <code>SWT.getPlatform()</code>.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
* </p>
*
* @return The active platform; never <code>null</code>.
*/
public final String getPlatform() {
return platform;
}
/**
* <p>
* Returns the prefix table.
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the active bindings are
* not yet computed, then this completes in <code>O(n)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @return A map of prefixes (<code>TriggerSequence</code>) to a map of
* available completions (possibly <code>null</code>, which means
* there is an exact match). The available completions is a map of
* trigger (<code>TriggerSequence</code>) to binding (<code>Binding</code>).
* This value will never be <code>null</code> but may be empty.
*/
private final Map getPrefixTable() {
if (prefixTable == null) {
recomputeBindings();
}
return Collections.unmodifiableMap(prefixTable);
}
/**
* <p>
* Gets the scheme with the given identifier. If the scheme does not already
* exist, then a new (undefined) scheme is created with that identifier.
* This guarantees that schemes will remain unique.
* </p>
* <p>
* This method completes in amortized <code>O(1)</code>.
* </p>
*
* @param schemeId
* The identifier for the scheme to retrieve; must not be
* <code>null</code>.
* @return A scheme with the given identifier.
*/
public final Scheme getScheme(final String schemeId) {
checkId(schemeId);
Scheme scheme = (Scheme) handleObjectsById.get(schemeId);
if (scheme == null) {
scheme = new Scheme(schemeId);
handleObjectsById.put(schemeId, scheme);
scheme.addSchemeListener(this);
}
return scheme;
}
/**
* <p>
* Ascends all of the parents of the scheme until no more parents are found.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the height of the context tree.
* </p>
*
* @param schemeId
* The id of the scheme for which the parents should be found;
* may be <code>null</code>.
* @return The array of scheme ids (<code>String</code>) starting with
* <code>schemeId</code> and then ascending through its ancestors.
*/
private final String[] getSchemeIds(String schemeId) {
final List strings = new ArrayList();
while (schemeId != null) {
strings.add(schemeId);
try {
schemeId = getScheme(schemeId).getParentId();
} catch (final NotDefinedException e) {
return new String[0];
}
}
return (String[]) strings.toArray(new String[strings.size()]);
}
/**
* <p>
* Returns whether the given trigger sequence is a partial match for the
* given sequence.
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the bindings aren't
* currently computed, then this completes in <code>O(n)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @param trigger
* The sequence which should be the prefix for some binding;
* should not be <code>null</code>.
* @return <code>true</code> if the trigger can be found in the active
* bindings; <code>false</code> otherwise.
*/
public final boolean isPartialMatch(final TriggerSequence trigger) {
return (getPrefixTable().get(trigger) != null);
}
/**
* <p>
* Returns whether the given trigger sequence is a perfect match for the
* given sequence.
* </p>
* <p>
* This method completes in <code>O(1)</code>. If the bindings aren't
* currently computed, then this completes in <code>O(n)</code>, where
* <code>n</code> is the number of bindings.
* </p>
*
* @param trigger
* The sequence which should match exactly; should not be
* <code>null</code>.
* @return <code>true</code> if the trigger can be found in the active
* bindings; <code>false</code> otherwise.
*/
public final boolean isPerfectMatch(final TriggerSequence trigger) {
return getActiveBindings().containsKey(trigger);
}
/**
* <p>
* Tests whether the locale for the binding matches one of the active
* locales.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of active locales.
* </p>
*
* @param binding
* The binding with which to test; must not be <code>null</code>.
* @return <code>true</code> if the binding's locale matches;
* <code>false</code> otherwise.
*/
private final boolean localeMatches(final Binding binding) {
boolean matches = false;
final String locale = binding.getLocale();
if (locale == null) {
return true; // shortcut a common case
}
for (int i = 0; i < locales.length; i++) {
if (Util.equals(locales[i], locale)) {
matches = true;
break;
}
}
return matches;
}
/**
* <p>
* Tests whether the platform for the binding matches one of the active
* platforms.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of active platforms.
* </p>
*
* @param binding
* The binding with which to test; must not be <code>null</code>.
* @return <code>true</code> if the binding's platform matches;
* <code>false</code> otherwise.
*/
private final boolean platformMatches(final Binding binding) {
boolean matches = false;
final String platform = binding.getPlatform();
if (platform == null) {
return true; // shortcut a common case
}
for (int i = 0; i < platforms.length; i++) {
if (Util.equals(platforms[i], platform)) {
matches = true;
break;
}
}
return matches;
}
/**
* <p>
* This recomputes the bindings based on changes to the state of the world.
* This computation can be triggered by changes to contexts, the active
* scheme, the locale, or the platform. This method tries to use the cache
* of pre-computed bindings, if possible. When this method completes,
* <code>activeBindings</code> will be set to the current set of bindings
* and <code>cachedBindings</code> will contain an instance of
* <code>CachedBindingSet</code> representing these bindings.
* </p>
* <p>
* This method completes in <code>O(n+pn)</code>, where <code>n</code>
* is the number of bindings, and <code>p</code> is the average number of
* triggers in a trigger sequence.
* </p>
*/
private final void recomputeBindings() {
if (bindings == null) {
// Not yet initialized. This is happening too early. Do nothing.
setActiveBindings(Collections.EMPTY_MAP, Collections.EMPTY_MAP,
Collections.EMPTY_MAP);
return;
}
// Figure out the current state.
final Set activeContextIds = new HashSet(contextManager
.getActiveContextIds());
final Map activeContextTree = createFilteredContextTreeFor(activeContextIds);
// Build a cached binding set for that state.
final CachedBindingSet bindingCache = new CachedBindingSet(
activeContextTree, locales, platforms, activeSchemeIds);
/*
* Check if the cached binding set already exists. If so, simply set the
* active bindings and return.
*/
CachedBindingSet existingCache = (CachedBindingSet) cachedBindings
.get(bindingCache);
if (existingCache == null) {
existingCache = bindingCache;
cachedBindings.put(existingCache, existingCache);
}
Map commandIdsByTrigger = existingCache.getBindingsByTrigger();
if (commandIdsByTrigger != null) {
if (DEBUG) {
Tracing.printTrace("BINDINGS", "Cache hit"); //$NON-NLS-1$ //$NON-NLS-2$
}
setActiveBindings(commandIdsByTrigger, existingCache
.getTriggersByCommandId(), existingCache.getPrefixTable());
return;
}
// There is no cached entry for this.
if (DEBUG) {
Tracing.printTrace("BINDINGS", "Cache miss"); //$NON-NLS-1$ //$NON-NLS-2$
}
// Compute the active bindings.
commandIdsByTrigger = new HashMap();
final Map triggersByParameterizedCommand = new HashMap();
computeBindings(activeContextTree, commandIdsByTrigger,
triggersByParameterizedCommand);
existingCache.setBindingsByTrigger(commandIdsByTrigger);
existingCache.setTriggersByCommandId(triggersByParameterizedCommand);
setActiveBindings(commandIdsByTrigger, triggersByParameterizedCommand,
buildPrefixTable(commandIdsByTrigger));
existingCache.setPrefixTable(prefixTable);
}
/**
* <p>
* Removes a listener from this binding manager.
* </p>
* <p>
* This method completes in amortized <code>O(1)</code>.
* </p>
*
* @param listener
* The listener to be removed; must not be <code>null</code>.
*/
public final void removeBindingManagerListener(
final IBindingManagerListener listener) {
removeListenerObject(listener);
}
/**
* <p>
* Removes any binding that matches the given values -- regardless of
* command identifier.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of bindings.
* </p>
*
* @param sequence
* The sequence to match; may be <code>null</code>.
* @param schemeId
* The scheme id to match; may be <code>null</code>.
* @param contextId
* The context id to match; may be <code>null</code>.
* @param locale
* The locale to match; may be <code>null</code>.
* @param platform
* The platform to match; may be <code>null</code>.
* @param windowManager
* The window manager to match; may be <code>null</code>. TODO
* Currently ignored.
* @param type
* The type to look for.
*
*/
public final void removeBindings(final TriggerSequence sequence,
final String schemeId, final String contextId, final String locale,
final String platform, final String windowManager, final int type) {
if ((bindings == null) || (bindingCount < 1)) {
return;
}
final Binding[] newBindings = new Binding[bindings.length];
boolean bindingsChanged = false;
int index = 0;
for (int i = 0; i < bindingCount; i++) {
final Binding binding = bindings[i];
boolean equals = true;
equals &= Util.equals(sequence, binding.getTriggerSequence());
equals &= Util.equals(schemeId, binding.getSchemeId());
equals &= Util.equals(contextId, binding.getContextId());
equals &= Util.equals(locale, binding.getLocale());
equals &= Util.equals(platform, binding.getPlatform());
equals &= (type == binding.getType());
if (equals) {
bindingsChanged = true;
} else {
newBindings[index++] = binding;
}
}
if (bindingsChanged) {
this.bindings = newBindings;
bindingCount = index;
clearCache();
}
}
/**
* <p>
* Attempts to remove deletion markers from the collection of bindings.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of bindings.
* </p>
*
* @param bindings
* The bindings from which the deleted items should be removed.
* This array should not be <code>null</code>, but may be
* empty.
* @return The array of bindings with the deletions removed; never
* <code>null</code>, but may be empty. Contains only instances
* of <code>Binding</code>.
*/
private final Binding[] removeDeletions(final Binding[] bindings) {
final Map deletions = new HashMap();
final Binding[] bindingsCopy = new Binding[bindingCount];
System.arraycopy(bindings, 0, bindingsCopy, 0, bindingCount);
int deletedCount = 0;
// Extract the deletions.
for (int i = 0; i < bindingCount; i++) {
final Binding binding = bindingsCopy[i];
if ((binding.getParameterizedCommand() == null)
&& (localeMatches(binding)) && (platformMatches(binding))) {
final TriggerSequence sequence = binding.getTriggerSequence();
final Object currentValue = deletions.get(sequence);
if (currentValue instanceof Binding) {
final Collection collection = new ArrayList(2);
collection.add(currentValue);
collection.add(binding);
deletions.put(sequence, collection);
} else if (currentValue instanceof Collection) {
final Collection collection = (Collection) currentValue;
collection.add(binding);
} else {
deletions.put(sequence, binding);
}
bindingsCopy[i] = null;
deletedCount++;
}
}
if (DEBUG) {
Tracing.printTrace("BINDINGS", "There are " + deletions.size() //$NON-NLS-1$ //$NON-NLS-2$
+ " deletion markers"); //$NON-NLS-1$
}
// Remove the deleted items.
for (int i = 0; i < bindingCount; i++) {
final Binding binding = bindingsCopy[i];
if (binding != null) {
final Object deletion = deletions.get(binding
.getTriggerSequence());
if (deletion instanceof Binding) {
if (((Binding) deletion).deletes(binding)) {
bindingsCopy[i] = null;
deletedCount++;
}
} else if (deletion instanceof Collection) {
final Collection collection = (Collection) deletion;
final Iterator iterator = collection.iterator();
while (iterator.hasNext()) {
final Object deletionBinding = iterator.next();
if (deletionBinding instanceof Binding) {
if (((Binding) deletionBinding).deletes(binding)) {
bindingsCopy[i] = null;
deletedCount++;
break;
}
}
}
}
}
}
// Compact the array.
final Binding[] returnValue = new Binding[bindingCount - deletedCount];
int index = 0;
for (int i = 0; i < bindingCount; i++) {
final Binding binding = bindingsCopy[i];
if (binding != null) {
returnValue[index++] = binding;
}
}
return returnValue;
}
/**
* <p>
* Attempts to resolve the conflicts for the given bindings -- irrespective
* of the currently active contexts. This means that type and scheme will be
* considered.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of bindings.
* </p>
*
* @param bindings
* The bindings which all match the same trigger sequence; must
* not be <code>null</code>, and should contain at least two
* items. This collection should only contain instances of
* <code>Binding</code> (i.e., no <code>null</code> values).
* @return The collection of bindings which match the current scheme.
*/
private final Collection resolveConflicts(final Collection bindings) {
final Collection matches = new ArrayList();
final Iterator bindingItr = bindings.iterator();
Binding bestMatch = (Binding) bindingItr.next();
matches.add(bestMatch);
/*
* Iterate over each binding and compares it with the best match. If a
* better match is found, then replace the best match and clear the
* collection. If the current binding is equivalent, then simply add it
* to the collection of matches. If the current binding is worse, then
* do nothing.
*/
while (bindingItr.hasNext()) {
final Binding current = (Binding) bindingItr.next();
/*
* SCHEME: Test whether the current is in a child scheme. Bindings
* defined in a child scheme will take priority over bindings
* defined in a parent scheme -- assuming that consulting their
* contexts led to a conflict.
*/
final String currentSchemeId = current.getSchemeId();
final String bestSchemeId = bestMatch.getSchemeId();
final int compareTo = compareSchemes(bestSchemeId, currentSchemeId);
if (compareTo > 0) {
bestMatch = current;
matches.clear();
matches.add(current);
}
if (compareTo != 0) {
continue;
}
/*
* TYPE: Test for type superiority.
*/
if (current.getType() > bestMatch.getType()) {
bestMatch = current;
matches.clear();
matches.add(current);
continue;
} else if (bestMatch.getType() > current.getType()) {
continue;
}
// The bindings are equivalent.
matches.add(current);
}
// Return all of the matches.
return matches;
}
/**
* <p>
* Attempts to resolve the conflicts for the given bindings.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of bindings.
* </p>
*
* @param bindings
* The bindings which all match the same trigger sequence; must
* not be <code>null</code>, and should contain at least two
* items. This collection should only contain instances of
* <code>Binding</code> (i.e., no <code>null</code> values).
* @param activeContextTree
* The tree of contexts to be used for all of the comparison. All
* of the keys should be active context identifiers (i.e., never
* <code>null</code>). The values will be their parents (i.e.,
* possibly <code>null</code>). Both keys and values are
* context identifiers (<code>String</code>). This map should
* never be empty, and must never be <code>null</code>.
* @return The binding which best matches the current state. If there is a
* tie, then return <code>null</code>.
*/
private final Binding resolveConflicts(final Collection bindings,
final Map activeContextTree) {
/*
* This flag is used to indicate when the bestMatch binding conflicts
* with another binding. We keep the best match binding so that we know
* if we find a better binding. However, if we don't find a better
* binding, then we known to return null.
*/
boolean conflict = false;
final Iterator bindingItr = bindings.iterator();
Binding bestMatch = (Binding) bindingItr.next();
/*
* Iterate over each binding and compare it with the best match. If a
* better match is found, then replace the best match and set the
* conflict flag to false. If a conflict is found, then leave the best
* match and set the conflict flag. Otherwise, just continue.
*/
while (bindingItr.hasNext()) {
final Binding current = (Binding) bindingItr.next();
/*
* SCHEME: Test whether the current is in a child scheme. Bindings
* defined in a child scheme will always take priority over bindings
* defined in a parent scheme.
*/
final String currentSchemeId = current.getSchemeId();
final String bestSchemeId = bestMatch.getSchemeId();
final int compareTo = compareSchemes(bestSchemeId, currentSchemeId);
if (compareTo > 0) {
bestMatch = current;
conflict = false;
}
if (compareTo != 0) {
continue;
}
/*
* CONTEXTS: Check for context superiority. Bindings defined in a
* child context will take priority over bindings defined in a
* parent context -- assuming that the schemes lead to a conflict.
*/
final String currentContext = current.getContextId();
final String bestContext = bestMatch.getContextId();
if (!currentContext.equals(bestContext)) {
boolean goToNextBinding = false;
// Ascend the current's context tree.
String contextPointer = currentContext;
while (contextPointer != null) {
if (contextPointer.equals(bestContext)) {
// the current wins
bestMatch = current;
conflict = false;
goToNextBinding = true;
break;
}
contextPointer = (String) activeContextTree
.get(contextPointer);
}
// Ascend the best match's context tree.
contextPointer = bestContext;
while (contextPointer != null) {
if (contextPointer.equals(currentContext)) {
// the best wins
goToNextBinding = true;
break;
}
contextPointer = (String) activeContextTree
.get(contextPointer);
}
if (goToNextBinding) {
continue;
}
}
/*
* TYPE: Test for type superiority.
*/
if (current.getType() > bestMatch.getType()) {
bestMatch = current;
conflict = false;
continue;
} else if (bestMatch.getType() > current.getType()) {
continue;
}
// We could not resolve the conflict between these two.
conflict = true;
}
// If the best match represents a conflict, then return null.
if (conflict) {
return null;
}
// Otherwise, we have a winner....
return bestMatch;
}
/**
* <p>
* Notifies this manager that a scheme has changed. This method is intended
* for internal use only.
* </p>
* <p>
* This method calls out to listeners, and so the time it takes to complete
* is dependent on third-party code.
* </p>
*
* @param schemeEvent
* An event describing the change in the scheme.
*/
public final void schemeChanged(final SchemeEvent schemeEvent) {
if (schemeEvent.isDefinedChanged()) {
final Scheme scheme = schemeEvent.getScheme();
final boolean schemeIdAdded = scheme.isDefined();
boolean activeSchemeChanged = false;
if (schemeIdAdded) {
definedHandleObjects.add(scheme);
} else {
definedHandleObjects.remove(scheme);
if (activeScheme == scheme) {
activeScheme = null;
activeSchemeIds = null;
activeSchemeChanged = true;
// Clear the binding solution.
clearSolution();
}
}
fireBindingManagerChanged(new BindingManagerEvent(this, false,
null, activeSchemeChanged, scheme, schemeIdAdded, false,
false));
}
}
/**
* Sets the active bindings and the prefix table. This ensures that the two
* values change at the same time, and that any listeners are notified
* appropriately.
*
* @param activeBindings
* This is a map of triggers ( <code>TriggerSequence</code>)
* to bindings (<code>Binding</code>). This value will only
* be <code>null</code> if the active bindings have not yet
* been computed. Otherwise, this value may be empty.
* @param activeBindingsByCommandId
* This is a map of fully-parameterized commands (<code>ParameterizedCommand</code>)
* to triggers ( <code>TriggerSequence</code>). This value
* will only be <code>null</code> if the active bindings have
* not yet been computed. Otherwise, this value may be empty.
* @param prefixTable
* A map of prefixes (<code>TriggerSequence</code>) to a map
* of available completions (possibly <code>null</code>, which
* means there is an exact match). The available completions is a
* map of trigger (<code>TriggerSequence</code>) to binding (<code>Binding</code>).
* This value may be <code>null</code> if there is no existing
* solution.
*/
private final void setActiveBindings(final Map activeBindings,
final Map activeBindingsByCommandId, final Map prefixTable) {
this.activeBindings = activeBindings;
final Map previousBindingsByParameterizedCommand = this.activeBindingsByParameterizedCommand;
this.activeBindingsByParameterizedCommand = activeBindingsByCommandId;
this.prefixTable = prefixTable;
fireBindingManagerChanged(new BindingManagerEvent(this, true,
previousBindingsByParameterizedCommand, false, null, false,
false, false));
}
/**
* <p>
* Selects one of the schemes as the active scheme. This scheme must be
* defined.
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the height of the context tree.
* </p>
*
* @param scheme
* The scheme to become active; must not be <code>null</code>.
* @throws NotDefinedException
* If the given scheme is currently undefined.
*/
public final void setActiveScheme(final Scheme scheme)
throws NotDefinedException {
if (scheme == null) {
throw new NullPointerException("Cannot activate a null scheme"); //$NON-NLS-1$
}
if ((scheme == null) || (!scheme.isDefined())) {
throw new NotDefinedException("Cannot activate an undefined scheme"); //$NON-NLS-1$
}
if (Util.equals(activeScheme, scheme)) {
return;
}
activeScheme = scheme;
activeSchemeIds = getSchemeIds(activeScheme.getId());
clearSolution();
fireBindingManagerChanged(new BindingManagerEvent(this, false, null,
true, null, false, false, false));
}
/**
* <p>
* Changes the set of bindings for this binding manager. Changing the set of
* bindings all at once ensures that: (1) duplicates are removed; and (2)
* avoids unnecessary intermediate computations. This method clears the
* existing bindings, but does not trigger a recomputation (other method
* calls are required to do that).
* </p>
* <p>
* This method completes in <code>O(n)</code>, where <code>n</code> is
* the number of bindings.
* </p>
*
* @param bindings
* The new array of bindings; may be <code>null</code>. This
* set is copied into a local data structure.
*/
public final void setBindings(final Binding[] bindings) {
if (Arrays.equals(this.bindings, bindings)) {
return; // nothing has changed
}
if ((bindings == null) || (bindings.length == 0)) {
this.bindings = null;
bindingCount = 0;
} else {
final int bindingsLength = bindings.length;
this.bindings = new Binding[bindingsLength];
System.arraycopy(bindings, 0, this.bindings, 0, bindingsLength);
bindingCount = bindingsLength;
}
clearCache();
}
/**
* <p>
* Changes the locale for this binding manager. The locale can be used to
* provide locale-specific bindings. If the locale is different than the
* current locale, this will force a recomputation of the bindings. The
* locale is in the same format as
* <code>Locale.getDefault().toString()</code>.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
* </p>
*
* @param locale
* The new locale; must not be <code>null</code>.
* @see Locale#getDefault()
*/
public final void setLocale(final String locale) {
if (locale == null) {
throw new NullPointerException("The locale cannot be null"); //$NON-NLS-1$
}
if (!Util.equals(this.locale, locale)) {
this.locale = locale;
this.locales = expand(locale, LOCALE_SEPARATOR);
clearSolution();
fireBindingManagerChanged(new BindingManagerEvent(this, false,
null, false, null, false, true, false));
}
}
/**
* <p>
* Changes the platform for this binding manager. The platform can be used
* to provide platform-specific bindings. If the platform is different than
* the current platform, then this will force a recomputation of the
* bindings. The locale is in the same format as
* <code>SWT.getPlatform()</code>.
* </p>
* <p>
* This method completes in <code>O(1)</code>.
* </p>
*
* @param platform
* The new platform; must not be <code>null</code>.
* @see org.eclipse.swt.SWT#getPlatform()
*/
public final void setPlatform(final String platform) {
if (platform == null) {
throw new NullPointerException("The platform cannot be null"); //$NON-NLS-1$
}
if (!Util.equals(this.platform, platform)) {
this.platform = platform;
this.platforms = expand(platform, Util.ZERO_LENGTH_STRING);
clearSolution();
fireBindingManagerChanged(new BindingManagerEvent(this, false,
null, false, null, false, false, true));
}
}
}