Mercurial > projects > dwt-addons
view dwtx/jface/bindings/BindingManager.d @ 90:7ffeace6c47f
Update 3.4M7 to 3.4
author | Frank Benoit <benoit@tionex.de> |
---|---|
date | Sun, 06 Jul 2008 23:30:07 +0200 |
parents | 4878bef4a38e |
children | 04b47443bb01 |
line wrap: on
line source
/******************************************************************************* * 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 dwtx.jface.bindings.BindingManager; import dwtx.jface.bindings.Binding; import dwtx.jface.bindings.BindingManagerEvent; import dwtx.jface.bindings.CachedBindingSet; import dwtx.jface.bindings.IBindingManagerListener; import dwtx.jface.bindings.ISchemeListener; import dwtx.jface.bindings.Scheme; import dwtx.jface.bindings.SchemeEvent; import dwtx.jface.bindings.Trigger; import dwtx.jface.bindings.TriggerSequence; // import java.io.BufferedWriter; // import java.io.IOException; // import java.io.StringWriter; import tango.util.collection.HashMap; import tango.util.collection.HashSet; import tango.util.collection.ArraySeq; import tango.util.collection.LinkSeq; import tango.util.collection.model.Seq; import tango.util.collection.model.Map; import tango.util.collection.model.Set; import tango.util.collection.model.View; import dwt.DWT; import dwtx.core.commands.CommandManager; import dwtx.core.commands.ParameterizedCommand; import dwtx.core.commands.common.HandleObjectManager; import dwtx.core.commands.common.NotDefinedException; import dwtx.core.commands.contexts.Context; import dwtx.core.commands.contexts.ContextManager; import dwtx.core.commands.contexts.ContextManagerEvent; import dwtx.core.commands.contexts.IContextManagerListener; import dwtx.core.commands.util.Tracing; import dwtx.core.runtime.IStatus; import dwtx.core.runtime.MultiStatus; import dwtx.core.runtime.Status; import dwtx.jface.bindings.keys.IKeyLookup; import dwtx.jface.bindings.keys.KeyLookupFactory; import dwtx.jface.bindings.keys.KeyStroke; import dwtx.jface.contexts.IContextIds; import dwtx.jface.internal.InternalPolicy; import dwtx.jface.util.Policy; import dwtx.jface.util.Util; import dwt.dwthelper.utils; 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 { private static Map!(Object,Object) EMPTY_MAP; /** * 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>Seq!(Object)</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!(Object,Object) map, Object key, Object value) { if (map is null) { return; } Object currentValue = map.get(key); if (currentValue !is null) { auto values = cast(Seq!(Object)) currentValue; values.append(value); } else { // currentValue is null auto values = new ArraySeq!(Object); values.append(value); map.add(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]; } auto strings = new LinkSeq!(String); auto 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.prepend(stringBuffer.toString()); } } strings.append(Util.ZERO_LENGTH_STRING); strings.append(cast(char[])null); return 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!(Object,Object) 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!(Object,Object) activeBindingsByParameterizedCommand = null; private Set!(Object) 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!(Object) 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!(Object,Object) 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!(Object,Object) 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!(Object); bindingErrors = new HashSet!(Object); cachedBindings = new HashMap!(Object,Object); locale = tango.text.Util.replace( Culture.current().toString().dup, '-', '_' ); locales = expand(locale, LOCALE_SEPARATOR); platform = DWT.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!(Object,Object) buildPrefixTable(Map!(Object,Object) activeBindings) { auto prefixTable = new HashMap!(Object,Object); foreach( k, v; activeBindings ){ TriggerSequence triggerSequence = cast(TriggerSequence)k; // Add the perfect match. if (!prefixTable.containsKey(triggerSequence)) { prefixTable.add(triggerSequence, null); } TriggerSequence[] prefixes = triggerSequence.getPrefixes(); int prefixesLength = prefixes.length; if (prefixesLength is 0) { continue; } // Break apart the trigger sequence. Binding binding = cast(Binding) v; for (int i = 0; i < prefixesLength; i++) { TriggerSequence prefix = prefixes[i]; Object value = prefixTable.get(prefix); if ((prefixTable.containsKey(prefix)) && (cast(Map!(Object,Object))value )) { (cast(Map!(Object,Object)) value).add(triggerSequence, binding); } else { auto map = new HashMap!(Object,Object); prefixTable.add(prefix, map); map.add(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!(Object,Object) activeContextTree, Map!(Object,Object) bindingsByTrigger, Map!(Object,Object) triggersByCommandId, Map!(Object,Object) 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. */ auto possibleBindings = new HashMap!(Object,Object); 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( new ArrayWrapperString(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); auto matches = new ArraySeq!(Object); matches.append(existingMatch); matches.append(binding); possibleBindings.add(trigger, matches); } else if (cast(Seq!(Object))existingMatch ) { auto matches = cast(Seq!(Object)) existingMatch; matches.append(binding); } else { possibleBindings.add(trigger, binding); } } MultiStatus conflicts = new MultiStatus("dwtx.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. */ foreach( k,v; possibleBindings ){ // Iterator possibleBindingItr = possibleBindings.entrySet() // .iterator(); // while (possibleBindingItr.hasNext()) { // Map.Entry entry = cast(Map.Entry) possibleBindingItr.next(); TriggerSequence trigger = cast(TriggerSequence) k;//entry.getKey(); Object match = v;//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. auto bindings = new ArraySeq!(Object); if (cast(Binding)match ) { bindings.append(match); bindingsByTrigger.add(trigger, bindings); addReverseLookup(triggersByCommandId, (cast(Binding) match) .getParameterizedCommand(), trigger); } else if (cast(View!(Object))match ) { bindings.append( (cast(View!(Object)) match).elements ); bindingsByTrigger.add(trigger, bindings); foreach( e; bindings ){ // Iterator matchItr = bindings.iterator(); // while (matchItr.hasNext()) { addReverseLookup(triggersByCommandId, (cast(Binding) e) .getParameterizedCommand(), trigger); } } } else { // We are building the flat map of trigger to commands. if (cast(Binding)match ) { Binding binding = cast(Binding) match; bindingsByTrigger.add(trigger, binding); addReverseLookup(triggersByCommandId, binding .getParameterizedCommand(), trigger); } else if (cast(Seq!(Object))match ) { Binding winner = resolveConflicts(cast(Seq!(Object)) match, activeContextTree); if (winner is null) { // warn once ... so as not to flood the logs conflictsByTrigger.add(trigger, match); if (!triggerConflicts.contains(trigger)) { 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(':'); foreach( e; cast(Seq!(Object)) match){ // Iterator i = (cast(Seq!(Object)) match).iterator(); // while (i.hasNext()) { sb.append('\n'); sb.append( e.toString() ); } } catch (IOException e) { // we should not get this } conflicts.add(new Status(IStatus.WARNING, "dwtx.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.add(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!(Object,Object) createContextTreeFor(Set!(Object) contextIds) { auto contextTree = new HashMap!(Object,Object); foreach( e; contextIds ){ auto childContextId = (cast(ArrayWrapperString)e).array; while (childContextId !is null) { // Check if we've already got the part of the tree from here up. if (contextTree.containsKey(/+childContextId+/e)) { break; } // Retrieve the context. Context childContext = contextManager .getContext(childContextId); // Add the child-parent pair to the tree. try { String parentContextId = childContext.getParentId(); contextTree.add(new ArrayWrapperString(childContextId), new ArrayWrapperString(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!(Object,Object) createFilteredContextTreeFor(Set!(Object) contextIds) { // Check to see whether a dialog or window is active. bool dialog = false; bool window = false; foreach( e; contextIds ){ String contextId = (cast(ArrayWrapperString) e).array; 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. */ foreach( e; contextIds.dup ){ String contextId = (cast(ArrayWrapperString) e).array; Context context = contextManager.getContext(contextId); try { String parentId = context.getParentId(); while (parentId !is null) { if (IContextIds.CONTEXT_ID_DIALOG.equals(parentId)) { if (!dialog) { contextIds.remove(e); // contextIdItr.remove(); } break; } if (IContextIds.CONTEXT_ID_WINDOW.equals(parentId)) { if (!window) { contextIds.remove(e); // contextIdItr.remove(); } break; } if (IContextIds.CONTEXT_ID_DIALOG_AND_WINDOW .equals(parentId)) { if ((!window) && (!dialog)) { contextIds.remove(e); // 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 contextIds.remove(e); // contextIdItr.remove(); // This is a logging optimization, only log the error once. if (context is null || !bindingErrors.contains(new ArrayWrapperString(context.getId()))) { if (context !is null) { bindingErrors.add(new ArrayWrapperString(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!(Object,Object) 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!(Object,Object) 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>Seq!(Object)</code> containing <code>Binding</code>). * This map may be empty, but it is never <code>null</code>. */ public final Map!(Object,Object) getActiveBindingsDisregardingContext() { if (bindings is null) { // Not yet initialized. This is happening too early. Do nothing. if( EMPTY_MAP is null ) EMPTY_MAP = new HashMap!(Object,Object); return 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.add(existingCache, existingCache); } auto commandIdsByTrigger = existingCache.getBindingsByTrigger(); if (commandIdsByTrigger !is null) { if (DEBUG) { Tracing.printTrace("BINDINGS", "Cache hit"); //$NON-NLS-1$ //$NON-NLS-2$ } return 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!(Object,Object); auto triggersByParameterizedCommand = new HashMap!(Object,Object); auto conflictsByTrigger = new HashMap!(Object,Object); 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>Seq!(Object)</code> containing <code>Binding</code>). * This map may be empty, but it is never <code>null</code>. * @since 3.2 */ private final Map!(Object,Object) getActiveBindingsDisregardingContextByParameterizedCommand() { if (bindings is null) { // Not yet initialized. This is happening too early. Do nothing. if( EMPTY_MAP is null ) EMPTY_MAP = new HashMap!(Object,Object); return 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.add(existingCache, existingCache); } auto 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. auto commandIdsByTrigger = new HashMap!(Object,Object); auto conflictsByTrigger = new HashMap!(Object,Object); triggersByParameterizedCommand = new HashMap!(Object,Object); 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 View!(Object) getActiveBindingsDisregardingContextFlat() { auto mergedBindings = new ArraySeq!(Object); foreach( k,v; getActiveBindingsDisregardingContext() ){ auto bindingCollection = cast(View!(Object))v; if ((bindingCollection !is null) && (!bindingCollection.drained())) { foreach( e; bindingCollection ){ mergedBindings.append(e); } } } 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(Seq!(Object))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(Seq!(Object))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 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; } auto 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()); } /** * <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!(Object,Object) getPartialMatches(TriggerSequence trigger) { auto partialMatches = cast(Map!(Object,Object)) getPrefixTable().get(trigger); if (partialMatches is null) { if( EMPTY_MAP is null ) EMPTY_MAP = new HashMap!(Object,Object); return 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>DWT.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!(Object,Object) 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.add(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) { auto strings = new ArraySeq!(Object); while (schemeId !is null) { strings.append( 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. if( EMPTY_MAP is null ) EMPTY_MAP = new HashMap!(Object,Object); setActiveBindings(EMPTY_MAP, EMPTY_MAP, EMPTY_MAP, EMPTY_MAP); return; } // Figure out the current state. auto activeContextIds = new HashSet!(Object); foreach( e; contextManager.getActiveContextIds()){ activeContextIds.add(stringcast(e)); } auto 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.add(existingCache, existingCache); } auto 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!(Object,Object); auto triggersByParameterizedCommand = new HashMap!(Object,Object); auto conflictsByTrigger = new HashMap!(Object,Object); 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!(Object,Object); 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 ) { auto collection = new ArraySeq!(Object); collection.append(currentValue); collection.append(binding); deletions.add(sequence, collection); } else if ( auto collection = cast(Seq!(Object))currentValue ) { collection.append(binding); } else { deletions.add(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(Seq!(Object))deletion ) { Seq!(Object) collection = cast(Seq!(Object)) deletion; foreach( e; collection){ // Iterator iterator = collection.iterator(); // while (iterator.hasNext()) { Object deletionBinding = e;//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(View!(Object) bindings, Map!(Object,Object) 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; bool first = true; foreach( b; bindings ){ if( first ){ first = false; bestMatch = cast(Binding) b;//bindingItr.next(); continue; } /* * 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) b;//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!(Object,Object) activeBindings, Map!(Object,Object) activeBindingsByCommandId, Map!(Object,Object) prefixTable, Map!(Object,Object) conflicts) { this.activeBindings = activeBindings; Map!(Object,Object) 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>DWT.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 dwt.DWT#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)); } } }