/*
* KeyMap.java
*
* Copyright (C) 2009-17 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.core.client.command;
// A KeyMap provides a two-way lookup between a KeySequence, and a BindableCommand:
// - Given a key sequence, one can discover commands bound to that key sequence,
// - Given a command, one can discover what key sequences it is bound to.
import java.util.ArrayList;
import java.util.List;
import org.rstudio.core.client.CommandWith2Args;
import org.rstudio.core.client.DirectedGraph;
import org.rstudio.core.client.Mutable;
import org.rstudio.core.client.DirectedGraph.DefaultConstructor;
import org.rstudio.core.client.DirectedGraph.ForEachNodeCommand;
import org.rstudio.core.client.command.KeyboardShortcut.KeyCombination;
import org.rstudio.core.client.command.KeyboardShortcut.KeySequence;
import org.rstudio.core.client.container.SafeMap;
public class KeyMap
{
public static enum KeyMapType {
ADDIN, EDITOR, APPLICATION;
}
public interface CommandBinding
{
public String getId();
public void execute();
public boolean isEnabled();
public boolean isUserDefinedBinding();
public AppCommand.Context getContext();
}
public KeyMap()
{
graph_ = new DirectedGraph<KeyCombination, List<CommandBinding>>(new DefaultConstructor<List<CommandBinding>>()
{
@Override
public List<CommandBinding> create()
{
return new ArrayList<CommandBinding>();
}
});
idToNodeMap_ = new SafeMap<String, List<DirectedGraph<KeyCombination, List<CommandBinding>>>>();
}
public void addBinding(KeySequence keys, CommandBinding command)
{
DirectedGraph<KeyCombination, List<CommandBinding>> node = graph_.ensureNode(keys.getData());
if (node.getValue() == null)
node.setValue(new ArrayList<CommandBinding>());
node.getValue().add(0, command);
if (!idToNodeMap_.containsKey(command.getId()))
idToNodeMap_.put(command.getId(), new ArrayList<DirectedGraph<KeyCombination, List<CommandBinding>>>());
idToNodeMap_.get(command.getId()).add(node);
}
public void setBindings(KeySequence keys, CommandBinding command)
{
clearBindings(command);
addBinding(keys, command);
}
public void setBindings(List<KeySequence> keyList, CommandBinding command)
{
clearBindings(command);
for (KeySequence keys : keyList)
addBinding(keys, command);
}
public void clearBindings(CommandBinding command)
{
if (!idToNodeMap_.containsKey(command.getId()))
return;
List<DirectedGraph<KeyCombination, List<CommandBinding>>> nodes = idToNodeMap_.get(command.getId());
for (DirectedGraph<KeyCombination, List<CommandBinding>> node : nodes)
{
List<CommandBinding> bindings = node.getValue();
if (bindings == null || bindings.isEmpty())
continue;
List<CommandBinding> filtered = new ArrayList<CommandBinding>();
for (CommandBinding binding : bindings)
if (binding.getId() != command.getId())
filtered.add(binding);
node.setValue(filtered);
}
idToNodeMap_.remove(command.getId());
}
public List<CommandBinding> getBindings(KeySequence keys)
{
DirectedGraph<KeyCombination, List<CommandBinding>> node = graph_.findNode(keys.getData());
if (node == null)
return null;
return node.getValue();
}
public List<KeySequence> getBindings(CommandBinding command)
{
return getBindings(command.getId());
}
public List<KeySequence> getBindings(String id)
{
List<KeySequence> keys = new ArrayList<KeySequence>();
List<DirectedGraph<KeyCombination, List<CommandBinding>>> bindings = idToNodeMap_.get(id);
if (bindings == null)
return keys;
for (int i = 0, n = bindings.size(); i < n; i++)
keys.add(new KeySequence(bindings.get(i).getKeyChain()));
return keys;
}
public CommandBinding getActiveBinding(KeySequence keys)
{
List<CommandBinding> commands = getBindings(keys);
if (commands == null)
return null;
for (CommandBinding command : commands)
if (command.isEnabled())
return command;
return null;
}
public boolean isPrefix(KeySequence keys)
{
DirectedGraph<KeyCombination, List<CommandBinding>> node = graph_.findNode(keys.getData());
if (node == null)
return false;
final Mutable<Boolean> pending = new Mutable<Boolean>(false);
node.forEachNode(new ForEachNodeCommand<KeyCombination, List<CommandBinding>>()
{
@Override
public boolean continueExecution(DirectedGraph<KeyCombination, List<CommandBinding>> node)
{
List<CommandBinding> bindings = node.getValue();
if (bindings == null || bindings.isEmpty())
return true;
for (CommandBinding binding : bindings)
{
if (binding.isEnabled())
{
pending.set(true);
return false;
}
}
return true;
}
});
return pending.get();
}
public void forEachBinding(final CommandWith2Args<KeySequence, List<CommandBinding>> command)
{
graph_.forEachNode(new ForEachNodeCommand<KeyCombination, List<CommandBinding>>()
{
@Override
public boolean continueExecution(DirectedGraph<KeyCombination, List<CommandBinding>> node)
{
command.execute(
new KeySequence(node.getKeyChain()),
node.getValue());
return true;
}
});
}
// Private members ----
// The actual graph used for dispatching key sequences to commands.
private final DirectedGraph<KeyCombination, List<CommandBinding>> graph_;
// Map used so we can quickly discover what bindings are active for a particular command.
private final SafeMap<String, List<DirectedGraph<KeyCombination, List<CommandBinding>>>> idToNodeMap_;
}