Mercurial > projects > dwt2
diff org.eclipse.jface/src/org/eclipse/jface/bindings/BindingManager.d @ 12:bc29606a740c
Added dwt-addons in original directory structure of eclipse.org
author | Frank Benoit <benoit@tionex.de> |
---|---|
date | Sat, 14 Mar 2009 18:23:29 +0100 |
parents | |
children | 6f068362a363 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/org.eclipse.jface/src/org/eclipse/jface/bindings/BindingManager.d Sat Mar 14 18:23:29 2009 +0100 @@ -0,0 +1,2326 @@ +/******************************************************************************* + * Copyright (c) 2004, 2008 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 + * Port to the D programming language: + * Frank Benoit <benoit@tionex.de> + *******************************************************************************/ +module org.eclipse.jface.bindings.BindingManager; + +import org.eclipse.jface.bindings.Binding; +import org.eclipse.jface.bindings.BindingManagerEvent; +import org.eclipse.jface.bindings.CachedBindingSet; +import org.eclipse.jface.bindings.IBindingManagerListener; +import org.eclipse.jface.bindings.ISchemeListener; +import org.eclipse.jface.bindings.Scheme; +import org.eclipse.jface.bindings.SchemeEvent; +import org.eclipse.jface.bindings.Trigger; +import org.eclipse.jface.bindings.TriggerSequence; + +// import java.io.BufferedWriter; +// import java.io.IOException; +// import java.io.StringWriter; + +import org.eclipse.swt.SWT; +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.core.runtime.IStatus; +import org.eclipse.core.runtime.MultiStatus; +import org.eclipse.core.runtime.Status; +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.internal.InternalPolicy; +import org.eclipse.jface.util.Policy; +import org.eclipse.jface.util.Util; + +import java.lang.all; +import java.util.Arrays; +import java.util.Collections; +import java.util.Collection; +import java.util.List; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.Map; +import java.util.HashMap; +import java.util.Set; +import java.util.HashSet; +static import tango.text.Text; +alias tango.text.Text.Text!(char) StringBuffer; +import tango.text.convert.Format; +import tango.text.locale.Core; +static import tango.text.Util; + +/** + * <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 : HandleObjectManager, + 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 bool DEBUG = false; + + /** + * Returned for optimized lookup. + */ + private static const TriggerSequence[] EMPTY_TRIGGER_SEQUENCE = null; + + /** + * The separator character used in locales. + */ + private static const 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(Map map, Object key, + Object value) { + if (map is null) { + return; + } + + Object currentValue = map.get(key); + if (currentValue !is null) { + Collection values = cast(Collection) currentValue; + values.add(value); + } else { // currentValue is null + auto 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, String separator) { + // Test for boundary conditions. + if (string is null || separator is null) { + return new String[0]; + } + + List strings = new ArrayList(); + StringBuffer stringBuffer = new StringBuffer(); + string = string.trim(); // remove whitespace + if (string.length > 0) { + + auto tokens = tango.text.Util.delimit(string, separator); + foreach( tok; tokens ){ + if (stringBuffer.length() > 0) { + stringBuffer.append(separator); + } + stringBuffer.append(tok.trim()); + strings.add(stringBuffer.toString()); + } + } + Collections.reverse(strings); + strings.add(Util.ZERO_LENGTH_STRING); + strings.add(""); + return stringcast(strings.toArray()); + } + + /** + * 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; + + private Set triggerConflicts; + + /** + * 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; + + /** + * A cache of context IDs that weren't defined. + */ + private Set bindingErrors; + + /** + * 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; + + /** + * 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 const 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 const ContextManager contextManager; + + /** + * The locale for this manager. This defaults to the current locale. The + * value will never be <code>null</code>. + */ + private String locale; + + /** + * 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; + + /** + * The platform for this manager. This defaults to the current platform. The + * value will never be <code>null</code>. + */ + private String platform; + + /** + * 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; + + /** + * 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 this(ContextManager contextManager, + CommandManager commandManager) { + triggerConflicts = new HashSet(); + bindingErrors = new HashSet(); + cachedBindings = new HashMap(); + locale = tango.text.Util.replace( Culture.current().toString().dup, '-', '_' ); + locales = expand(locale, LOCALE_SEPARATOR); + + platform = SWT.getPlatform(); + platforms = expand(platform, Util.ZERO_LENGTH_STRING); + if (contextManager is null) { + throw new NullPointerException( + "A binding manager requires a context manager"); //$NON-NLS-1$ + } + + if (commandManager is 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(Binding binding) { + if (binding is null) { + throw new NullPointerException("Cannot add a null binding"); //$NON-NLS-1$ + } + + if (bindings is null) { + bindings = new Binding[1]; + } else if (bindingCount >= bindings.length) { + 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( + IBindingManagerListener listener) { + addListenerObject(cast(Object)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(Map activeBindings) { + Map prefixTable = new HashMap; + Iterator bindingItr = activeBindings.entrySet().iterator(); + while (bindingItr.hasNext()) { + Map.Entry entry = cast(Map.Entry) bindingItr.next(); + TriggerSequence triggerSequence = cast(TriggerSequence) entry + .getKey(); + + // Add the perfect match. + if (!prefixTable.containsKey(triggerSequence)) { + prefixTable.put(triggerSequence, cast(Object)null); + } + + TriggerSequence[] prefixes = triggerSequence.getPrefixes(); + int prefixesLength = prefixes.length; + if (prefixesLength is 0) { + continue; + } + + // Break apart the trigger sequence. + Binding binding = cast(Binding) entry.getValue(); + for (int i = 0; i < prefixesLength; i++) { + TriggerSequence prefix = prefixes[i]; + Object value = prefixTable.get(prefix); + if ((prefixTable.containsKey(prefix)) && (cast(Map)value )) { + (cast(Map) value).put(triggerSequence, binding); + } else { + Map map = new HashMap(); + prefixTable.put(prefix, cast(Object)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, 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(String schemeId1, + String schemeId2) { + if (!schemeId2.equals(schemeId1)) { + for (int i = 0; i < activeSchemeIds.length; i++) { + 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(Map activeContextTree, + Map bindingsByTrigger, Map triggersByCommandId, + Map conflictsByTrigger) { + /* + * FIRST PASS: Remove all of the bindings that are marking deletions. + */ + 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. + */ + Map possibleBindings = new HashMap(); + int length = trimmedBindings.length; + for (int i = 0; i < length; i++) { + Binding binding = trimmedBindings[i]; + bool found; + + // Check the context. + String contextId = binding.getContextId(); + if ((activeContextTree !is null) + && (!activeContextTree.containsKey( stringcast(contextId)))) { + continue; + } + + // Check the locale. + if (!localeMatches(binding)) { + continue; + } + + // Check the platform. + if (!platformMatches(binding)) { + continue; + } + + // Check the scheme ids. + String schemeId = binding.getSchemeId(); + found = false; + if (activeSchemeIds !is null) { + for (int j = 0; j < activeSchemeIds.length; j++) { + if (Util.opEquals(schemeId, activeSchemeIds[j])) { + found = true; + break; + } + } + } + if (!found) { + continue; + } + + // Insert the match into the list of possible matches. + TriggerSequence trigger = binding.getTriggerSequence(); + Object existingMatch = possibleBindings.get(trigger); + if (cast(Binding)existingMatch ) { + possibleBindings.remove(trigger); + Collection matches = new ArrayList; + matches.add(existingMatch); + matches.add(binding); + possibleBindings.put(trigger, cast(Object)matches); + + } else if (cast(Collection)existingMatch ) { + auto matches = cast(Collection) existingMatch; + matches.add(binding); + + } else { + possibleBindings.put(trigger, binding); + } + } + + MultiStatus conflicts = new MultiStatus("org.eclipse.jface", 0, //$NON-NLS-1$ + "Keybinding conflicts occurred. They may interfere with normal accelerator operation.", //$NON-NLS-1$ + null); + /* + * 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. + */ + Iterator possibleBindingItr = possibleBindings.entrySet() + .iterator(); + while (possibleBindingItr.hasNext()) { + Map.Entry entry = cast(Map.Entry) possibleBindingItr.next(); + TriggerSequence trigger = cast(TriggerSequence) entry.getKey(); + 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 is null) { + // We are building the list of all possible bindings. + Collection bindings = new ArrayList; + if (cast(Binding)match ) { + bindings.add(match); + bindingsByTrigger.put(trigger, cast(Object)bindings); + addReverseLookup(triggersByCommandId, (cast(Binding) match) + .getParameterizedCommand(), trigger); + + } else if (cast(Collection)match ) { + bindings.addAll( cast(Collection) match); + bindingsByTrigger.put(trigger, cast(Object)bindings); + + Iterator matchItr = bindings.iterator(); + while (matchItr.hasNext()) { + addReverseLookup(triggersByCommandId, + (cast(Binding) matchItr.next()) + .getParameterizedCommand(), trigger); + } + } + + } else { + // We are building the flat map of trigger to commands. + if (cast(Binding)match ) { + Binding binding = cast(Binding) match; + bindingsByTrigger.put(trigger, binding); + addReverseLookup(triggersByCommandId, binding + .getParameterizedCommand(), trigger); + + } else if (cast(Collection)match ) { + Binding winner = resolveConflicts(cast(Collection) match, + activeContextTree); + if (winner is null) { + // warn once ... so as not to flood the logs + conflictsByTrigger.put(trigger, match); + if (!triggerConflicts.add(trigger)) { +// StringWriter sw = new StringWriter(); +// BufferedWriter buffer = new BufferedWriter(sw); + StringBuffer sb = new StringBuffer(); + try { + sb.append("A conflict occurred for "); //$NON-NLS-1$ + sb.append(trigger.toString()); + sb.append(':'); + Iterator i = (cast(Collection) match).iterator(); + while (i.hasNext()) { + sb.append('\n'); + sb.append( i.next().toString() ); + } + } catch (IOException e) { + // we should not get this + } + conflicts.add(new Status(IStatus.WARNING, + "org.eclipse.jface", //$NON-NLS-1$ + sb.toString())); + } + if (DEBUG) { + Tracing.printTrace("BINDINGS", //$NON-NLS-1$ + "A conflict occurred for " ~ trigger.toString); //$NON-NLS-1$ + Tracing.printTrace("BINDINGS", " " ~ match.toString); //$NON-NLS-1$ //$NON-NLS-2$ + } + } else { + bindingsByTrigger.put(trigger, winner); + addReverseLookup(triggersByCommandId, winner + .getParameterizedCommand(), trigger); + } + } + } + } + if (conflicts.getSeverity() !is IStatus.OK) { + Policy.getLog().log(conflicts); + } + } + + /** + * <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( + ContextManagerEvent contextManagerEvent) { + if (contextManagerEvent.isActiveContextsChanged()) { +// clearSolution(); + recomputeBindings(); + } + } + + /** + * 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(Trigger[] triggers) { + int strokeCount = triggers.length; + for (int i = 0; i < triggers.length; i++) { + Trigger trigger = triggers[i]; + if (cast(KeyStroke)trigger ) { + KeyStroke keyStroke = cast(KeyStroke) trigger; + int modifierKeys = keyStroke.getModifierKeys(); + IKeyLookup lookup = KeyLookupFactory.getDefault(); + if ((modifierKeys & lookup.getAlt()) !is 0) { + strokeCount += 8; + } + if ((modifierKeys & lookup.getCtrl()) !is 0) { + strokeCount += 2; + } + if ((modifierKeys & lookup.getShift()) !is 0) { + strokeCount += 4; + } + if ((modifierKeys & lookup.getCommand()) !is 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(Set contextIds) { + Map contextTree = new HashMap; + + final Iterator contextIdItr = contextIds.iterator(); + while (contextIdItr.hasNext()) { + Object childContextIdObj = contextIdItr.next(); + String childContextId = stringcast (childContextIdObj); + + while (childContextId !is null) { + // Check if we've already got the part of the tree from here up. + if (contextTree.containsKey(childContextIdObj)) { + break; + } + + // Retrieve the context. + Context childContext = contextManager + .getContext(childContextId); + + // Add the child-parent pair to the tree. + try { + String parentContextId = childContext.getParentId(); + contextTree.put(childContextIdObj, stringcast(parentContextId)); + childContextId = parentContextId; + } catch (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(Set contextIds) { + // Check to see whether a dialog or window is active. + bool dialog = false; + bool window = false; + Iterator contextIdItr = contextIds.iterator(); + while (contextIdItr.hasNext()) { + String contextId = stringcast(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. + */ + contextIdItr = contextIds.iterator(); + while (contextIdItr.hasNext()) { + String contextId = stringcast( contextIdItr.next()); + Context context = contextManager.getContext(contextId); + try { + String parentId = context.getParentId(); + while (parentId !is 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) { + // since this context was part of an undefined hierarchy, + // I'm going to yank it out as a bad bet + contextIdItr.remove(); + + // This is a logging optimization, only log the error once. + if (context is null || !bindingErrors.contains(stringcast(context.getId()))) { + if (context !is null) { + bindingErrors.add(stringcast(context.getId())); + } + + // now log like you've never logged before! + Policy.getLog().log(new Status( IStatus.ERROR, Policy.JFACE, IStatus.OK, + "Undefined context while filtering dialog/window contexts", //$NON-NLS-1$ + e)); + } + } + } + + 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(BindingManagerEvent event) { + if (event is null) { + throw new NullPointerException(); + } + + Object[] listeners = getListeners(); + for (int i = 0; i < listeners.length; i++) { + IBindingManagerListener listener = cast(IBindingManagerListener) listeners[i]; + listener.bindingManagerChanged(event); + } + } + + /** + * <p> + * Returns the active bindings. The caller must not modify the returned map. + * </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 is null) { + recomputeBindings(); + } + + return activeBindings; + } + + /** + * <p> + * Returns the active bindings indexed by command identifier. The caller + * must not modify the returned map. + * </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 is null) { + recomputeBindings(); + } + + return 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 is null) { + // Not yet initialized. This is happening too early. Do nothing. + return Collections.EMPTY_MAP; + } + + // Build a cached binding set for that state. + 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 = cast(CachedBindingSet) cachedBindings + .get(bindingCache); + if (existingCache is null) { + existingCache = bindingCache; + cachedBindings.put(existingCache, existingCache); + } + Map commandIdsByTrigger = existingCache.getBindingsByTrigger(); + if (commandIdsByTrigger !is 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(); + Map triggersByParameterizedCommand = new HashMap(); + Map conflictsByTrigger = new HashMap(); + computeBindings(null, commandIdsByTrigger, + triggersByParameterizedCommand, conflictsByTrigger); + existingCache.setBindingsByTrigger(commandIdsByTrigger); + existingCache.setTriggersByCommandId(triggersByParameterizedCommand); + existingCache.setConflictsByTrigger(conflictsByTrigger); + 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 is null) { + // Not yet initialized. This is happening too early. Do nothing. + return Collections.EMPTY_MAP; + } + + // Build a cached binding set for that state. + 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 = cast(CachedBindingSet) cachedBindings + .get(bindingCache); + if (existingCache is null) { + existingCache = bindingCache; + cachedBindings.put(existingCache, existingCache); + } + Map triggersByParameterizedCommand = existingCache + .getTriggersByCommandId(); + if (triggersByParameterizedCommand !is 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. + Map commandIdsByTrigger = new HashMap(); + Map conflictsByTrigger = new HashMap(); + triggersByParameterizedCommand = new HashMap(); + computeBindings(null, commandIdsByTrigger, + triggersByParameterizedCommand, conflictsByTrigger); + existingCache.setBindingsByTrigger(commandIdsByTrigger); + existingCache.setTriggersByCommandId(triggersByParameterizedCommand); + existingCache.setConflictsByTrigger(conflictsByTrigger); + + 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() { + Collection bindingCollections = getActiveBindingsDisregardingContext() + .values(); + Collection mergedBindings = new ArrayList(); + Iterator bindingCollectionItr = bindingCollections.iterator(); + while (bindingCollectionItr.hasNext()) { + Collection bindingCollection = cast(Collection) bindingCollectionItr + .next(); + if ((bindingCollection !is 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( + ParameterizedCommand parameterizedCommand) { + Object object = getActiveBindingsDisregardingContextByParameterizedCommand() + .get(parameterizedCommand); + if (auto collection = cast(Collection)object ) { + return arraycast!(TriggerSequence)( collection + .toArray()); + } + return EMPTY_TRIGGER_SEQUENCE; + } + + /** + * <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( + ParameterizedCommand parameterizedCommand) { + Object object = getActiveBindingsByParameterizedCommand().get( + parameterizedCommand); + if ( auto collection = cast(Collection)object ) { + return arraycast!(TriggerSequence)(collection + .toArray(new TriggerSequence[collection.size()])); + } + + return EMPTY_TRIGGER_SEQUENCE; + } + + /** + * <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(String commandId) { + ParameterizedCommand parameterizedCommand = new ParameterizedCommand( + commandManager.getCommand(commandId), null); + return getActiveBindingsFor(parameterizedCommand); + } + + /** + * A variation on {@link BindingManager#getActiveBindingsFor(String)} that + * returns an array of bindings, rather than trigger sequences. This method + * is needed for doing "best" calculations 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(ParameterizedCommand command) { + TriggerSequence[] triggers = getActiveBindingsFor(command); + if (triggers.length is 0) { + return null; + } + + Map activeBindings = getActiveBindings(); + if (activeBindings !is null) { + Binding[] bindings = new Binding[triggers.length]; + for (int i = 0; i < triggers.length; i++) { + TriggerSequence triggerSequence = triggers[i]; + Object object = activeBindings.get(triggerSequence); + Binding binding = cast(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. + * Bindings which belong to a particular locale or platform are given + * preference over those that do not. 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(String commandId) { + return getBestActiveBindingFor(new ParameterizedCommand(commandManager.getCommand(commandId), null)); + } + + /** + * @param command + * @return + * a trigger sequence, or <code>null</code> + * @since 3.4 + */ + public final TriggerSequence getBestActiveBindingFor(ParameterizedCommand command) { + final Binding[] bindings = getActiveBindingsFor1(command); + if ((bindings is null) || (bindings.length is 0)) { + return null; + } + + Binding bestBinding = bindings[0]; + int compareTo; + for (int i = 1; i < bindings.length; i++) { + Binding currentBinding = bindings[i]; + + // Bindings in a child scheme are always given preference. + String bestSchemeId = bestBinding.getSchemeId(); + String currentSchemeId = currentBinding.getSchemeId(); + compareTo = compareSchemes(bestSchemeId, currentSchemeId); + if (compareTo > 0) { + bestBinding = currentBinding; + } + if (compareTo !is 0) { + continue; + } + + /* + * Bindings with a locale are given preference over those that do + * not. + */ + String bestLocale = bestBinding.getLocale(); + String currentLocale = currentBinding.getLocale(); + if ((bestLocale is null) && (currentLocale !is null)) { + bestBinding = currentBinding; + } + if (!(Util.opEquals(bestLocale, currentLocale))) { + continue; + } + + /* + * Bindings with a platform are given preference over those that do + * not. + */ + String bestPlatform = bestBinding.getPlatform(); + String currentPlatform = currentBinding.getPlatform(); + if ((bestPlatform is null) && (currentPlatform !is null)) { + bestBinding = currentBinding; + } + if (!(Util.opEquals(bestPlatform, currentPlatform))) { + continue; + } + + /* + * Check to see which has the least number of triggers in the + * trigger sequence. + */ + TriggerSequence bestTriggerSequence = bestBinding + .getTriggerSequence(); + TriggerSequence currentTriggerSequence = currentBinding + .getTriggerSequence(); + Trigger[] bestTriggers = bestTriggerSequence.getTriggers(); + Trigger[] currentTriggers = currentTriggerSequence + .getTriggers(); + compareTo = bestTriggers.length - currentTriggers.length; + if (compareTo > 0) { + bestBinding = currentBinding; + } + if (compareTo !is 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 !is 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(); + } + + /** + * Gets the formatted string representing 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 formatted string for the best binding; may be + * <code>null</code> if no bindings are active for the given + * command. + * @since 3.2 + */ + public final String getBestActiveBindingFormattedFor(String commandId) { + TriggerSequence binding = getBestActiveBindingFor(commandId); + if (binding !is null) { + return binding.format(); + } + + return null; + } + /** + * <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 is null) { + return null; + } + + 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 arraycast!(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(TriggerSequence trigger) { + Map partialMatches = cast(Map) getPrefixTable().get(trigger); + if (partialMatches is 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(TriggerSequence trigger) { + return cast(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. The caller must not modify the returned map. + * </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 is null) { + recomputeBindings(); + } + + return 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(String schemeId) { + checkId(schemeId); + + Scheme scheme = cast(Scheme) handleObjectsById.get(schemeId); + if (scheme is 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) { + List strings = new ArrayList(); + while (schemeId !is null) { + strings.add( stringcast(schemeId)); + try { + schemeId = getScheme(schemeId).getParentId(); + } catch (NotDefinedException e) { + Policy.getLog().log( new Status( + IStatus.ERROR, Policy.JFACE, IStatus.OK, + "Failed ascending scheme parents", //$NON-NLS-1$ + e)); + return null; + } + } + + return stringcast(strings.toArray()); + } + + /** + * <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 bool isPartialMatch(TriggerSequence trigger) { + return (getPrefixTable().get(trigger) !is 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 bool isPerfectMatch(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 bool localeMatches(Binding binding) { + bool matches = false; + + String locale = binding.getLocale(); + if (locale is null) { + return true; // shortcut a common case + } + + for (int i = 0; i < locales.length; i++) { + if (Util.opEquals(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 bool platformMatches(Binding binding) { + bool matches = false; + + String platform = binding.getPlatform(); + if (platform is null) { + return true; // shortcut a common case + } + + for (int i = 0; i < platforms.length; i++) { + if (Util.opEquals(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 is null) { + // Not yet initialized. This is happening too early. Do nothing. + setActiveBindings(Collections.EMPTY_MAP, Collections.EMPTY_MAP, + Collections.EMPTY_MAP, Collections.EMPTY_MAP); + return; + } + + // Figure out the current state. + Set activeContextIds = new HashSet(contextManager + .getActiveContextIds()); + Map activeContextTree = createFilteredContextTreeFor(activeContextIds); + + // Build a cached binding set for that state. + 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 = cast(CachedBindingSet) cachedBindings + .get(bindingCache); + if (existingCache is null) { + existingCache = bindingCache; + cachedBindings.put(existingCache, existingCache); + } + Map commandIdsByTrigger = existingCache.getBindingsByTrigger(); + if (commandIdsByTrigger !is null) { + if (DEBUG) { + Tracing.printTrace("BINDINGS", "Cache hit"); //$NON-NLS-1$ //$NON-NLS-2$ + } + setActiveBindings(commandIdsByTrigger, existingCache + .getTriggersByCommandId(), existingCache.getPrefixTable(), + existingCache.getConflictsByTrigger()); + 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(); + Map triggersByParameterizedCommand = new HashMap(); + Map conflictsByTrigger = new HashMap(); + computeBindings(activeContextTree, commandIdsByTrigger, + triggersByParameterizedCommand, conflictsByTrigger); + existingCache.setBindingsByTrigger(commandIdsByTrigger); + existingCache.setTriggersByCommandId(triggersByParameterizedCommand); + existingCache.setConflictsByTrigger(conflictsByTrigger); + setActiveBindings(commandIdsByTrigger, triggersByParameterizedCommand, + buildPrefixTable(commandIdsByTrigger), + conflictsByTrigger); + existingCache.setPrefixTable(prefixTable); + } + + /** + * <p> + * Remove the specific binding by identity. Does nothing if the binding is + * not in the manager. + * </p> + * <p> + * This method completes in <code>O(n)</code>, where <code>n</code> is + * the number of bindings. + * </p> + * + * @param binding + * The binding to be removed; must not be <code>null</code>. + * @since 3.2 + */ + public final void removeBinding(Binding binding) { + if (bindings is null || bindings.length < 1) { + return; + } + + Binding[] newBindings = new Binding[bindings.length]; + bool bindingsChanged = false; + int index = 0; + for (int i = 0; i < bindingCount; i++) { + Binding b = bindings[i]; + if (b is binding) { + bindingsChanged = true; + } else { + newBindings[index++] = b; + } + } + + if (bindingsChanged) { + this.bindings = newBindings; + bindingCount = index; + clearCache(); + } + } + + /** + * <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( + IBindingManagerListener listener) { + removeListenerObject(cast(Object)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(TriggerSequence sequence, + String schemeId, String contextId, String locale, + String platform, String windowManager, int type) { + if ((bindings is null) || (bindingCount < 1)) { + return; + } + + Binding[] newBindings = new Binding[bindings.length]; + bool bindingsChanged = false; + int index = 0; + for (int i = 0; i < bindingCount; i++) { + Binding binding = bindings[i]; + bool equals = true; + equals &= Util.opEquals(sequence, binding.getTriggerSequence()); + equals &= Util.opEquals(schemeId, binding.getSchemeId()); + equals &= Util.opEquals(contextId, binding.getContextId()); + equals &= Util.opEquals(locale, binding.getLocale()); + equals &= Util.opEquals(platform, binding.getPlatform()); + equals &= (type is 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(Binding[] bindings) { + auto deletions = new HashMap; + 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++) { + Binding binding = bindingsCopy[i]; + if ((binding.getParameterizedCommand() is null) + && (localeMatches(binding)) && (platformMatches(binding))) { + TriggerSequence sequence = binding.getTriggerSequence(); + Object currentValue = deletions.get(sequence); + if (cast(Binding)currentValue ) { + Collection collection = new ArrayList; + collection.add(currentValue); + collection.add(binding); + deletions.put(sequence, cast(Object)collection); + } else if ( auto collection = cast(Collection)currentValue ) { + collection.add(binding); + } else { + deletions.put(sequence, binding); + } + bindingsCopy[i] = null; + deletedCount++; + } + } + + if (DEBUG) { + Tracing.printTrace("BINDINGS", Format("There are {} deletion markers", deletions.size()) //$NON-NLS-1$ //$NON-NLS-2$ + ); //$NON-NLS-1$ + } + + // Remove the deleted items. + for (int i = 0; i < bindingCount; i++) { + Binding binding = bindingsCopy[i]; + if (binding !is null) { + Object deletion = deletions.get(binding + .getTriggerSequence()); + if (cast(Binding)deletion ) { + if ((cast(Binding) deletion).deletes(binding)) { + bindingsCopy[i] = null; + deletedCount++; + } + + } else if (cast(Collection)deletion ) { + Collection collection = cast(Collection) deletion; + Iterator iterator = collection.iterator(); + while (iterator.hasNext()) { + Object deletionBinding = iterator.next(); + if (cast(Binding)deletionBinding ) { + if ((cast(Binding) deletionBinding).deletes(binding)) { + bindingsCopy[i] = null; + deletedCount++; + break; + } + } + } + + } + } + } + + // Compact the array. + Binding[] returnValue = new Binding[bindingCount - deletedCount]; + int index = 0; + for (int i = 0; i < bindingCount; i++) { + Binding binding = bindingsCopy[i]; + if (binding !is null) { + returnValue[index++] = binding; + } + } + + return returnValue; + } + + /** + * <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(Collection bindings, + 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. + */ + bool conflict = false; + + Iterator bindingItr = bindings.iterator(); + Binding bestMatch = cast(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()) { + Binding current = cast(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. + */ + String currentSchemeId = current.getSchemeId(); + String bestSchemeId = bestMatch.getSchemeId(); + int compareTo = compareSchemes(bestSchemeId, currentSchemeId); + if (compareTo > 0) { + bestMatch = current; + conflict = false; + } + if (compareTo !is 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. + */ + String currentContext = current.getContextId(); + String bestContext = bestMatch.getContextId(); + if (!currentContext.equals(bestContext)) { + bool goToNextBinding = false; + + // Ascend the current's context tree. + String contextPointer = currentContext; + while (contextPointer !is null) { + if (contextPointer.equals(bestContext)) { + // the current wins + bestMatch = current; + conflict = false; + goToNextBinding = true; + break; + } + contextPointer = stringcast(activeContextTree + .get(stringcast(contextPointer))); + } + + // Ascend the best match's context tree. + contextPointer = bestContext; + while (contextPointer !is null) { + if (contextPointer.equals(currentContext)) { + // the best wins + goToNextBinding = true; + break; + } + contextPointer = stringcast( activeContextTree + .get(stringcast(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(SchemeEvent schemeEvent) { + if (schemeEvent.isDefinedChanged()) { + Scheme scheme = schemeEvent.getScheme(); + bool schemeIdAdded = scheme.isDefined(); + bool activeSchemeChanged = false; + if (schemeIdAdded) { + definedHandleObjects.add(scheme); + } else { + definedHandleObjects.remove(scheme); + + if (activeScheme is scheme) { + activeScheme = null; + activeSchemeIds = null; + activeSchemeChanged = true; + + // Clear the binding solution. + clearSolution(); + } + } + + if (isListenerAttached()) { + 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(Map activeBindings, + Map activeBindingsByCommandId, Map prefixTable, + Map conflicts) { + this.activeBindings = activeBindings; + Map previousBindingsByParameterizedCommand = this.activeBindingsByParameterizedCommand; + this.activeBindingsByParameterizedCommand = activeBindingsByCommandId; + this.prefixTable = prefixTable; + InternalPolicy.currentConflicts = conflicts; + + 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(Scheme scheme) { + if (scheme is null) { + throw new NullPointerException("Cannot activate a null scheme"); //$NON-NLS-1$ + } + + if ((scheme is null) || (!scheme.isDefined())) { + throw new NotDefinedException( + "Cannot activate an undefined scheme. " //$NON-NLS-1$ + ~ scheme.getId()); + } + + if (Util.opEquals(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(Binding[] bindings) { + if (Arrays.equals(this.bindings, bindings)) { + return; // nothing has changed + } + + if ((bindings is null) || (bindings.length is 0)) { + this.bindings = null; + bindingCount = 0; + } else { + 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(String locale) { + if (locale is null) { + throw new NullPointerException("The locale cannot be null"); //$NON-NLS-1$ + } + + if (!Util.opEquals(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(String platform) { + if (platform is null) { + throw new NullPointerException("The platform cannot be null"); //$NON-NLS-1$ + } + + if (!Util.opEquals(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)); + } + } +}