/* * ShortcutsEmitter.java * * Copyright (C) 2009-12 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.rebind.command; import com.google.gwt.core.ext.TreeLogger; import com.google.gwt.core.ext.TreeLogger.Type; import com.google.gwt.core.ext.UnableToCompleteException; import com.google.gwt.user.rebind.SourceWriter; import org.rstudio.core.client.Pair; import org.rstudio.core.client.command.KeyboardShortcut; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import javax.xml.transform.Result; import javax.xml.transform.TransformerFactory; import javax.xml.transform.dom.DOMSource; import java.io.StringWriter; import java.util.ArrayList; import java.util.List; public class ShortcutsEmitter { public ShortcutsEmitter(TreeLogger logger, String groupName, Element shortcutsEl) throws UnableToCompleteException { logger_ = logger; shortcutsEl_ = shortcutsEl; groupName_ = groupName; } public void generate(SourceWriter writer) throws UnableToCompleteException { NodeList children = shortcutsEl_.getChildNodes(); for (int i = 0; i < children.getLength(); i++) { Node childNode = children.item(i); if (childNode.getNodeType() != Node.ELEMENT_NODE) continue; Element childEl = (Element)childNode; if (!childEl.getTagName().equals("shortcut")) { logger_.log(Type.ERROR, "Unexpected element: " + elementToString(childEl)); throw new UnableToCompleteException(); } String condition = childEl.getAttribute("if"); String command = childEl.getAttribute("refid"); String shortcutValue = childEl.getAttribute("value"); String title = childEl.getAttribute("title"); String disableModes = childEl.getAttribute("disableModes"); // Use null when we don't have a command associated with the shortcut, // otherwise refer to the function that returns the command command += command.isEmpty() ? "null" : "()"; if (shortcutValue.length() == 0) { logger_.log(Type.ERROR, "Required attribute shortcut was missing\n" + elementToString(childEl)); throw new UnableToCompleteException(); } List<String> shortcuts = preprocessShortcutValue(shortcutValue); for (String shortcut : shortcuts) { printShortcut(writer, condition, shortcut, command, groupName_, title, disableModes); } } } private static List<String> preprocessShortcutValue(String shortcutValue) { List<String> shortcuts = new ArrayList<String>(); for (String keySequence : shortcutValue.split("\\|")) { if (keySequence.indexOf("Cmd") != -1) { shortcuts.add(keySequence.replaceAll("Cmd", "Ctrl")); shortcuts.add(keySequence.replaceAll("Cmd", "Meta")); } else { shortcuts.add(keySequence); } } return shortcuts; } private void printShortcut(SourceWriter writer, String condition, String shortcut, String command, String shortcutGroup, String title, String disableModes) throws UnableToCompleteException { List<Pair<Integer, String>> keys = new ArrayList<Pair<Integer, String>>(); for (String keyCombination : shortcut.split("\\s+")) { String[] chunks = keyCombination.split("\\+"); // Build the shortcut modifiers integer and validate // as we build. int modifiers = KeyboardShortcut.NONE; for (int i = 0; i < chunks.length - 1; i++) { String m = chunks[i]; if (m.equals("Ctrl")) modifiers += KeyboardShortcut.CTRL; else if (m.equals("Alt")) modifiers += KeyboardShortcut.ALT; else if (m.equals("Shift")) modifiers += KeyboardShortcut.SHIFT; else if (m.equals("Meta")) modifiers += KeyboardShortcut.META; else { logger_.log( Type.ERROR, "Invalid modifier '" + m + "'; expected one of " + "'Ctrl', 'Alt', 'Shift', 'Meta'"); throw new UnableToCompleteException(); } } // Validate the key name. String key = toKey(chunks[chunks.length - 1]); if (key == null) { logger_.log(Type.ERROR, "Invalid shortcut '" + shortcut + "', only " + "modified alphanumeric characters, enter, " + "left, right, up, down, pageup, pagedown, " + "and tab are valid"); throw new UnableToCompleteException(); } // Push the parsed keys to the list. keys.add(new Pair<Integer, String>(modifiers, key)); } // Emit the relevant code registering these shortcuts. if (!condition.isEmpty()) { writer.println("if (" + condition + ") {"); writer.indent(); } if (keys.size() == 1) { int modifiers = keys.get(0).first; String key = keys.get(0).second; writer.println("ShortcutManager.INSTANCE.register(" + modifiers + ", " + key + ", " + command + ", " + "\"" + shortcutGroup + "\", " + "\"" + title + "\", " + "\"" + disableModes + "\");"); } else if (keys.size() == 2) { int m1 = keys.get(0).first; String k1 = keys.get(0).second; int m2 = keys.get(1).first; String k2 = keys.get(1).second; writer.println("ShortcutManager.INSTANCE.register(" + m1 + ", " + k1 + ", " + m2 + ", " + k2 + ", " + command + ", " + "\"" + shortcutGroup + "\", " + "\"" + title + "\", " + "\"" + disableModes + "\");"); } else { logger_.log( Type.ERROR, "Invalid key sequence: sequences must be of length 1 or 2"); throw new UnableToCompleteException(); } if (!condition.isEmpty()) { writer.outdent(); writer.println("}"); } } private String toKey(String val) { if (val.matches("^[a-zA-Z0-9]$")) return "'" + val.toUpperCase() + "'"; if (val.equals("/")) return "191"; if (val.equalsIgnoreCase("enter")) return "com.google.gwt.event.dom.client.KeyCodes.KEY_ENTER"; if (val.equalsIgnoreCase("right")) return "com.google.gwt.event.dom.client.KeyCodes.KEY_RIGHT"; if (val.equalsIgnoreCase("left")) return "com.google.gwt.event.dom.client.KeyCodes.KEY_LEFT"; if (val.equalsIgnoreCase("up")) return "com.google.gwt.event.dom.client.KeyCodes.KEY_UP"; if (val.equalsIgnoreCase("down")) return "com.google.gwt.event.dom.client.KeyCodes.KEY_DOWN"; if (val.equalsIgnoreCase("tab")) return "com.google.gwt.event.dom.client.KeyCodes.KEY_TAB"; if (val.equalsIgnoreCase("pageup")) return "com.google.gwt.event.dom.client.KeyCodes.KEY_PAGEUP"; if (val.equalsIgnoreCase("pagedown")) return "com.google.gwt.event.dom.client.KeyCodes.KEY_PAGEDOWN"; if (val.equalsIgnoreCase("F1")) return "112"; if (val.equalsIgnoreCase("F2")) return "113"; if (val.equalsIgnoreCase("F3")) return "114"; if (val.equalsIgnoreCase("F4")) return "115"; if (val.equalsIgnoreCase("F5")) return "116"; if (val.equalsIgnoreCase("F6")) return "117"; if (val.equalsIgnoreCase("F7")) return "118"; if (val.equalsIgnoreCase("F8")) return "119"; if (val.equalsIgnoreCase("F9")) return "120"; if (val.equalsIgnoreCase("F10")) return "121"; if (val.equalsIgnoreCase("F11")) return "122"; if (val.equalsIgnoreCase("F12")) return "123"; if (val.equals("`")) return "192"; if (val.equals(".")) return "190"; if (val.equals("=")) return "187"; if (val.equals(",")) return "188"; if (val.equals("-")) return "189"; if (val.equals("Backspace")) return "8"; return null; } private String elementToString(Element el) throws UnableToCompleteException { try { javax.xml.transform.TransformerFactory tfactory = TransformerFactory.newInstance(); javax.xml.transform.Transformer xform = tfactory.newTransformer(); javax.xml.transform.Source src = new DOMSource(el); java.io.StringWriter writer = new StringWriter(); Result result = new javax.xml.transform.stream.StreamResult(writer); xform.transform(src, result); return writer.toString(); } catch (Exception e) { logger_.log(Type.ERROR, "Error attempting to stringify some XML", e); throw new UnableToCompleteException(); } } private final TreeLogger logger_; private final Element shortcutsEl_; private final String groupName_; }