/*
* Copyright (c) 2012, the Dart project authors.
*
* Licensed under the Eclipse Public License v1.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at
*
* http://www.eclipse.org/legal/epl-v10.html
*
* Unless required by applicable law or agreed to in writing, software distributed under the License
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.dart.tools.ui.internal.preferences;
import com.google.dart.tools.core.DartCore;
import com.google.dart.tools.core.utilities.io.FileUtilities;
import com.google.dart.tools.ui.DartToolsPlugin;
import com.google.dart.tools.ui.internal.DartUiException;
import com.google.dart.tools.ui.internal.DartUiStatus;
import org.eclipse.core.commands.Command;
import org.eclipse.core.commands.CommandManager;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.common.NotDefinedException;
import org.eclipse.core.commands.contexts.ContextManager;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.BindingManager;
import org.eclipse.jface.bindings.Scheme;
import org.eclipse.jface.bindings.keys.KeyBinding;
import org.eclipse.jface.bindings.keys.KeySequence;
import org.eclipse.jface.bindings.keys.ParseException;
import org.eclipse.jface.preference.IPreferenceStore;
import org.eclipse.ui.activities.IActivityManager;
import org.eclipse.ui.commands.ICommandService;
import org.eclipse.ui.keys.IBindingService;
import org.w3c.dom.Comment;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
/**
* Defines persistence operations for Dart Editor key bindings.
* <p>
* The key binding file is an XML file with the following schema:
*
* <pre>
* dartKeyBindings - Root element containing a list of keyBinding
* keyBinding - Key binding element with attributes:
* commandName - The user-readable name of the command (as it appears in the UI)
* customKeySequence - The user-customized key sequence (default to dartKeySequence)
* dartKeySequence - The standard key sequence defined by Dart Editor
* platform - The platform name for platform-specific bindings (optional)
* </pre>
* TODO allow empty command name to unbind a key sequence<br>
* TODO allow empty keys to remove all bindings for a command
*/
public class DartKeyBindingPersistence {
private class KeyBindingHandler extends DefaultHandler {
private List<Map<String, String>> bindings;
private int version;
private Map<String, String> attribs;
@Override
public void endElement(String uri, String localName, String qName) {
if (qName.equals(XML_NODE_BINDING)) {
bindings.add(attribs);
attribs = null;
}
}
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes)
throws SAXException {
if (qName.equals(XML_NODE_BINDING)) {
if (version != 1) {
// only check version if it has content
throw new SAXException();
}
attribs = new HashMap<String, String>();
String commandName = attributes.getValue(XML_ATTRIBUTE_COMMANDID);
attribs.put(XML_ATTRIBUTE_COMMANDID, commandName);
String context = attributes.getValue(XML_ATTRIBUTE_CONTEXTID);
attribs.put(XML_ATTRIBUTE_CONTEXTID, context);
String dartkeys = attributes.getValue(XML_ATTRIBUTE_KEYS);
attribs.put(XML_ATTRIBUTE_KEYS, dartkeys);
String platform = attributes.getValue(XML_ATTRIBUTE_PLATFORM);
attribs.put(XML_ATTRIBUTE_PLATFORM, platform);
} else if (qName.equals(XML_NODE_ROOT)) {
bindings = new ArrayList<Map<String, String>>();
try {
String vers = attributes.getValue(XML_ATTRIBUTE_VERSION);
if (vers != null) {
version = Integer.parseInt(vers);
}
} catch (NumberFormatException ex) {
throw new SAXException(ex);
}
}
}
List<Map<String, String>> getBindings() {
return bindings;
}
}
public static final String CUSTOM_KEY_BINDING_STRING = DartToolsPlugin.PLUGIN_ID + ".keyBindings";
private static final String SERIALIZATION_PROBLEM = "Problems serializing key bindings to XML."; //$NON-NLS-1$
private static final String DESERIALIZATION_PROBLEM = "Problems reading key bindings from XML."; //$NON-NLS-1$
private static final String DART_BINDING_SCHEME = "com.google.dart.tools.dartAcceleratorConfiguration"; //$NON-NLS-1$
private static final String XML_NODE_ROOT = "dartKeyBindings"; //$NON-NLS-1$
private static final String XML_NODE_BINDING = "keyBinding"; //$NON-NLS-1$
private static final String XML_ATTRIBUTE_VERSION = "version"; //$NON-NLS-1$
// the key sequence names are chosen to be adjacent after lexically sorting attribute names
private static final String XML_ATTRIBUTE_KEYS = "keySequence"; //$NON-NLS-1$
// the command name is first in a lexical sort of attribute names
private static final String XML_ATTRIBUTE_COMMANDID = "commandName"; //$NON-NLS-1$
private static final String XML_ATTRIBUTE_CONTEXTID = "context"; //$NON-NLS-1$
private static final String XML_ATTRIBUTE_PLATFORM = "platform"; //$NON-NLS-1$// optional attribute
private static final String XML_UNKNOWN = ""; //$NON-NLS-1$ // should never be used
private static final String DESCR_FORMAT = "The format is straightforward, consisting of three attributes plus one that is optional.\n"
+ "The required attributes are the command name, which is the same as it appears in\n"
+ "menus, and the key sequence, which is all uppercase. The context is an internal identifier\n"
+ "used to indicate in which portion of the UI the binding is active. The optional attribute\n"
+ "is the name of the platform to which the binding applies if it is not universal.";
private static DartUiException createException(Throwable ex, String message) {
return new DartUiException(DartUiStatus.createError(IStatus.ERROR, message, ex));
}
/**
* The workbench's activity manager. This activity manager is used to see if certain commands
* should be filtered from the user interface.
*/
private IActivityManager activityManager;
/**
* The workbench's binding service. This binding service is used to access the current set of
* bindings, and to persist changes.
*/
private IBindingService bindingService;
/**
* A local copy of the workbench's binding manager. Changes are made locally while processing the
* new bindings. When everything is complete and error-free the changes are persisted.
*/
private BindingManager bindingManager;
/**
* A map of binding elements that are being written.
*/
private Map<String, Element> knownBindings;
private ICommandService commandService;
public DartKeyBindingPersistence(IActivityManager activityManager,
IBindingService bindingService, ICommandService commandService) {
this.activityManager = activityManager;
this.bindingService = bindingService;
this.commandService = commandService;
}
public Binding findBinding(String commandName, String platform, String context)
throws NotDefinedException {
Binding[] bindings = bindingService.getBindings();
if (bindings != null) {
for (Binding binding : bindings) {
if (binding.getSchemeId().equals(DART_BINDING_SCHEME)
&& (context == null || context.equals(binding.getContextId()))) {
if ((platform != null && platform.equals(binding.getPlatform()))
|| binding.getPlatform() == null) {
ParameterizedCommand pc = binding.getParameterizedCommand();
if (pc != null) {
Command cmd = pc.getCommand();
if (cmd != null) {
try {
if (commandName.equals(pc.getName())) {
return binding;
}
} catch (NotDefinedException e) {
DartCore.logError("Dropping key binding for " + commandName);
}
}
}
}
}
}
}
return null;
}
/**
* Read a key binding file in the format created by {@code writeFile()}.
*
* @param file The File of key bindings
* @throws CoreException if there is a problem reading or parsing the file
*/
public void readFile(File file, String encoding) throws CoreException {
Reader reader = null;
try {
reader = new FileReader(file);
readFrom(reader);
} catch (IOException ex) {
throw createException(ex, ex.getMessage());
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException ex) {
DartToolsPlugin.log(ex);
}
}
}
try {
String bindString = FileUtilities.getContents(file, encoding);
IPreferenceStore prefs = DartToolsPlugin.getDefault().getPreferenceStore();
prefs.setValue(CUSTOM_KEY_BINDING_STRING, bindString);
} catch (IOException ex) {
DartToolsPlugin.log(ex);
}
}
/**
* Read key bindings in the format created by {@code writeFile()}.
*
* @param reader The Reader used to read the input
* @throws CoreException if there is a problem reading or parsing the file
*/
public void readFrom(Reader reader) throws CoreException {
initBindingManager();
bindingManager.setBindings(new Binding[0]);
List<Map<String, String>> newBindings;
newBindings = readKeyBindingsFromStream(new InputSource(reader));
for (Map<String, String> map : newBindings) {
updateKeyBinding(map);
}
saveKeyBindingPreferences();
}
/**
* Remove all custom key bindings and restore default bindings.
*/
public void resetBindings() throws CoreException {
IPreferenceStore prefs = DartToolsPlugin.getDefault().getPreferenceStore();
prefs.setValue(CUSTOM_KEY_BINDING_STRING, "");
bindingService.readRegistryAndPreferences(commandService);
initBindingManager(); // deletes all USER bindings
saveKeyBindingPreferences();
}
/**
* Called at start up; restore custom bindings saved in a previous session, if any.
*/
public void restoreBindingPreferences() {
IPreferenceStore prefs = DartToolsPlugin.getDefault().getPreferenceStore();
String prefString = prefs.getString(CUSTOM_KEY_BINDING_STRING);
if (prefString == null || prefString.isEmpty()) {
return;
}
Reader reader = new StringReader(prefString);
try {
readFrom(reader);
} catch (CoreException ex) {
DartToolsPlugin.log(ex);
}
}
/**
* Write the currently-defined key bindings to a file. This file is intended to be edited by users
* to create custom key bindings. For usability, ID strings are not written. For readability, an
* XML format is used so that each value has a label indicating its purpose. The current key
* binding is written twice, once as a reference that is used to identify the binding when the
* file is read, and another time as a template for the user to edit. The command name is written
* to help the user identify which command is being modified. A platform identifier is included if
* it exists; this is used to define platform-specific bindings, which are common on Mac OSX.
*
* @param file The File to write
* @param encoding The file encoding to use
* @throws CoreException if there are any problem writing the file
*/
public void writeFile(File file, String encoding) throws CoreException {
try {
OutputStream stream = new FileOutputStream(file);
try {
writeKeyBindingsToStream(stream, encoding);
} finally {
try {
stream.close();
} catch (IOException ex) {
DartToolsPlugin.log(ex);
}
}
} catch (IOException e) {
throw createException(e, SERIALIZATION_PROBLEM);
}
}
private Element createBindingElement(Binding binding, Document document) {
// binding is known to have a ParameterizedCommand whose command ID matches a registered Command
String keys = binding.getTriggerSequence().toString();
String platform = binding.getPlatform();
String context = binding.getContextId();
System.out.println(context);
String commandName;
try {
commandName = binding.getParameterizedCommand().getName();
} catch (NotDefinedException ex) {
return null;
}
String id = keys + commandName + (platform == null ? "" : platform) + context;
if (knownBindings.containsKey(id)) {
if (binding.getType() == Binding.USER) {
// A SYSTEM binding has already been created
return null; // do not add it again
} else {
// A USER binding has already been created; update its standard key binding
Element element = knownBindings.get(id);
element.setAttribute(XML_ATTRIBUTE_KEYS, binding.getTriggerSequence().toString());
return null;
}
}
Element element = document.createElement(XML_NODE_BINDING);
element.setAttribute(XML_ATTRIBUTE_KEYS, keys);
element.setAttribute(XML_ATTRIBUTE_COMMANDID, commandName);
element.setAttribute(XML_ATTRIBUTE_CONTEXTID, context);
if (platform != null) {
element.setAttribute(XML_ATTRIBUTE_PLATFORM, platform);
}
knownBindings.put(id, element);
return element;
}
private void initBindingManager() {
bindingManager = new BindingManager(new ContextManager(), new CommandManager());
Scheme[] definedSchemes = bindingService.getDefinedSchemes();
try {
for (int i = 0; i < definedSchemes.length; i++) {
Scheme scheme = definedSchemes[i];
Scheme copy = bindingManager.getScheme(scheme.getId());
copy.define(scheme.getName(), scheme.getDescription(), scheme.getParentId());
}
bindingManager.setActiveScheme(bindingService.getActiveScheme());
} catch (NotDefinedException e) {
throw new Error("Internal error in DartKeyBindingPersistence"); //$NON-NLS-1$
}
bindingManager.setLocale(bindingService.getLocale());
bindingManager.setPlatform(bindingService.getPlatform());
Binding[] currentBindings = bindingService.getBindings();
Set<Binding> trimmedBindings = new HashSet<Binding>();
if (currentBindings != null) {
for (Binding binding : currentBindings) {
if (binding.getType() != Binding.USER) {
trimmedBindings.add(binding);
}
}
}
Binding[] trimmedBindingArray = trimmedBindings.toArray(new Binding[trimmedBindings.size()]);
bindingManager.setBindings(trimmedBindingArray);
}
private boolean isActive(Command command) {
return activityManager.getIdentifier(command.getId()).isEnabled();
}
private List<Map<String, String>> readKeyBindingsFromStream(InputSource inputSource)
throws CoreException {
KeyBindingHandler handler = new KeyBindingHandler();
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(inputSource, handler);
} catch (SAXException e) {
throw createException(e, DESERIALIZATION_PROBLEM);
} catch (IOException e) {
throw createException(e, DESERIALIZATION_PROBLEM);
} catch (ParserConfigurationException e) {
throw createException(e, DESERIALIZATION_PROBLEM);
}
return handler.getBindings();
}
/**
* Serialize the current key bindings and store them in the preference for next startup.
*/
private void saveKeyBindingPreferences() throws DartUiException {
try {
bindingService.savePreferences(bindingManager.getActiveScheme(), bindingManager.getBindings());
} catch (IOException e) {
throw createException(e, DESERIALIZATION_PROBLEM);
}
}
private Binding[] sort(Binding[] bindings) {
Comparator<Binding> comp = new Comparator<Binding>() {
@Override
public int compare(Binding b0, Binding b1) {
ParameterizedCommand c0 = b0.getParameterizedCommand();
ParameterizedCommand c1 = b1.getParameterizedCommand();
int k;
if (c0 == null || c1 == null) {
if (c0 != c1) {
k = c0 == null ? -1 : 1;
} else {
k = 0;
}
} else {
try {
k = c0.getCommand().getName().compareTo(c1.getCommand().getName());
} catch (NotDefinedException ex) {
k = 0;
}
}
if (k == 0) {
String p0 = b0.getPlatform();
if (p0 == null) {
p0 = XML_UNKNOWN;
}
String p1 = b1.getPlatform();
if (p1 == null) {
p1 = XML_UNKNOWN;
}
k = p0.compareTo(p1);
}
if (k == 0) {
k = b0.getTriggerSequence().toString().compareTo(b1.getTriggerSequence().toString());
}
return k;
}
};
Arrays.sort(bindings, comp);
return bindings;
}
private void updateKeyBinding(Map<String, String> map) throws CoreException {
try {
String platform = map.get(XML_ATTRIBUTE_PLATFORM);
String commandName = map.get(XML_ATTRIBUTE_COMMANDID);
String context = map.get(XML_ATTRIBUTE_CONTEXTID);
String stdKeys = map.get(XML_ATTRIBUTE_KEYS);
Binding binding = findBinding(commandName, platform, context);
if (binding == null) {
return;
}
Command command = binding.getParameterizedCommand().getCommand();
ParameterizedCommand cmd = new ParameterizedCommand(command, null);
String schemeId = binding.getSchemeId();
String contextId = binding.getContextId();
String locale = binding.getLocale();
String wm = null;
int type = Binding.USER;
KeySequence stdSeq = KeySequence.getInstance(stdKeys);
Binding newBind = new KeyBinding(stdSeq, cmd, schemeId, contextId, locale, platform, wm, type);
bindingManager.removeBindings(stdSeq, schemeId, contextId, null, platform, null, type);
bindingManager.addBinding(newBind);
} catch (NotDefinedException ex) {
throw createException(ex, ex.getMessage());
} catch (ParseException ex) {
throw createException(ex, ex.getMessage());
}
}
private void writeKeyBindingsToStream(OutputStream stream, String encoding) throws CoreException {
try {
knownBindings = new HashMap<String, Element>();
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.newDocument();
Element rootElement = document.createElement(XML_NODE_ROOT);
rootElement.setAttribute(XML_ATTRIBUTE_VERSION, Integer.toString(1));
document.appendChild(rootElement);
Comment comment = document.createComment(DESCR_FORMAT);
document.getElementsByTagName(XML_NODE_ROOT).item(0).appendChild(comment);
Binding[] bindings = bindingService.getBindings();
if (bindings != null) {
bindings = sort(bindings);
for (Binding binding : bindings) {
if (binding.getSchemeId().equals(DART_BINDING_SCHEME)) {
ParameterizedCommand pc = binding.getParameterizedCommand();
if (pc != null) {
Command cmd = pc.getCommand();
if (cmd != null && isActive(cmd)) {
Element bindingElement = createBindingElement(binding, document);
if (bindingElement != null) {
rootElement.appendChild(bindingElement);
}
}
}
}
}
}
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.METHOD, "xml"); //$NON-NLS-1$
transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
transformer.setOutputProperty(OutputKeys.INDENT, "yes"); //$NON-NLS-1$
transformer.transform(new DOMSource(document), new StreamResult(stream));
} catch (TransformerException e) {
throw createException(e, SERIALIZATION_PROBLEM);
} catch (ParserConfigurationException e) {
throw createException(e, SERIALIZATION_PROBLEM);
}
}
}