// Copyright 2012 Google Inc. All Rights Reserved. // // Licensed under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0 // // 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.collide.client.editor.input; import com.google.collide.client.editor.Editor; import com.google.collide.client.editor.Spacer; import com.google.collide.client.editor.selection.SelectionModel; import com.google.collide.client.editor.selection.SelectionModel.MoveAction; import com.google.collide.client.util.input.CharCodeWithModifiers; import com.google.collide.client.util.input.KeyCodeMap; import com.google.collide.client.util.input.ModifierKeys; import com.google.collide.shared.document.LineInfo; import com.google.collide.shared.util.SortedList; import org.waveprotocol.wave.client.common.util.JsoIntMap; import org.waveprotocol.wave.client.common.util.SignalEvent; import org.waveprotocol.wave.client.common.util.UserAgent; import elemental.events.KeyboardEvent; import java.util.Random; /** * The default InputScheme implementation for common keybindings. {@link ModifierKeys#ACTION} * for action key abstraction details. * <ul> * <li>ACTION+S - prevent default save behavior from browser. * <li>ACTION+A - select all text in the current document. * <li>TAB - if there is a selection region then insert a tab at the beginning * of each line the selection passes through, else insert a tab at the current * position. * <li>SHIFT+TAB - remove a tab character if possible from the beginning of each * line the current selection passes through. * <li>BACKSPACE - delete one character to the left of the cursor. DELETE - * delete one character to the right of the cursor. * <li>CUT (ACTION+X)/COPY (ACTION+C)/PASTE (ACTION+V) - same as native * cut/copy/paste functionality. * <li>UNDO (ACTION+Z)/REDO (ACTION+Y) - undo or redo the most recent change * to the document. * <li>Cursor movement (ARROW_*, PAGE_*) - change the position of the cursor * based upon the directional key pressed and the unit of movement associated * with that key. For arrow keys this is one character in the direction of the * arrow, for page up/down this is an entire page. * </ul> * * */ public class DefaultScheme extends InputScheme { private static final boolean ENABLE_DEBUG_SPACER_KEYS = false; private static final boolean ENABLE_ANIMATION_CONTROL_KEYS = false; static final JsoIntMap<MoveAction> MOVEMENT_KEYS_MAPPING = JsoIntMap.create(); InputMode defaultMode; static { //Initialize key mappings. registerMovementKey(ModifierKeys.NONE, KeyCodeMap.ARROW_LEFT, MoveAction.LEFT); registerMovementKey(ModifierKeys.NONE, KeyCodeMap.ARROW_RIGHT, MoveAction.RIGHT); registerMovementKey(ModifierKeys.NONE, KeyCodeMap.ARROW_UP, MoveAction.UP); registerMovementKey(ModifierKeys.NONE, KeyCodeMap.ARROW_DOWN, MoveAction.DOWN); registerMovementKey(ModifierKeys.NONE, KeyCodeMap.PAGE_UP, MoveAction.PAGE_UP); registerMovementKey(ModifierKeys.NONE, KeyCodeMap.PAGE_DOWN, MoveAction.PAGE_DOWN); if (UserAgent.isMac()) { registerMovementKey(ModifierKeys.ALT, KeyCodeMap.ARROW_LEFT, MoveAction.WORD_LEFT); registerMovementKey(ModifierKeys.ALT, KeyCodeMap.ARROW_RIGHT, MoveAction.WORD_RIGHT); registerMovementKey(ModifierKeys.ACTION, KeyCodeMap.ARROW_LEFT, MoveAction.LINE_START); registerMovementKey(ModifierKeys.ACTION, KeyCodeMap.ARROW_RIGHT, MoveAction.LINE_END); registerMovementKey(ModifierKeys.NONE, KeyCodeMap.HOME, MoveAction.TEXT_START); registerMovementKey(ModifierKeys.NONE, KeyCodeMap.END, MoveAction.TEXT_END); /* * Add Emacs-style bindings on Mac (note that these will not conflict * with any Collide shortcuts since these chord with CTRL, and Collide * shortcuts chord with CMD) */ registerMovementKey(ModifierKeys.CTRL, 'a', MoveAction.LINE_START); registerMovementKey(ModifierKeys.CTRL, 'e', MoveAction.LINE_END); registerMovementKey(ModifierKeys.CTRL, 'p', MoveAction.UP); registerMovementKey(ModifierKeys.CTRL, 'n', MoveAction.DOWN); registerMovementKey(ModifierKeys.CTRL, 'f', MoveAction.RIGHT); registerMovementKey(ModifierKeys.CTRL, 'b', MoveAction.LEFT); } else { registerMovementKey(ModifierKeys.ACTION, KeyCodeMap.ARROW_LEFT, MoveAction.WORD_LEFT); registerMovementKey(ModifierKeys.ACTION, KeyCodeMap.ARROW_RIGHT, MoveAction.WORD_RIGHT); registerMovementKey(ModifierKeys.NONE, KeyCodeMap.HOME, MoveAction.LINE_START); registerMovementKey(ModifierKeys.NONE, KeyCodeMap.END, MoveAction.LINE_END); registerMovementKey(ModifierKeys.ACTION, KeyCodeMap.HOME, MoveAction.TEXT_START); registerMovementKey(ModifierKeys.ACTION, KeyCodeMap.END, MoveAction.TEXT_END); } } private static void registerMovementKey(int modifiers, int charCode, MoveAction action) { MOVEMENT_KEYS_MAPPING.put(CharCodeWithModifiers.computeKeyDigest(modifiers, charCode), action); } /** * Setup and add single InputMode */ public DefaultScheme(InputController inputController) { super(inputController); defaultMode = new InputMode() { @Override public void setup() { } @Override public void teardown() { } /** * By default, check for cursor movement, then add this as text to the * current document. * * Only add printable text, not control characters. */ @Override public boolean onDefaultInput(SignalEvent event, char character) { // Check for movement here. int letter = KeyCodeMap.getKeyFromEvent(event); int modifiers = ModifierKeys.computeModifiers(event); boolean withShift = (modifiers & ModifierKeys.SHIFT) != 0; modifiers &= ~ModifierKeys.SHIFT; int strippedKeyDigest = CharCodeWithModifiers.computeKeyDigest(modifiers, letter); if (MOVEMENT_KEYS_MAPPING.containsKey(strippedKeyDigest)) { MoveAction action = MOVEMENT_KEYS_MAPPING.get(strippedKeyDigest); getScheme().getInputController().getSelection().move(action, withShift); return true; } if (event.getAltKey()) { // Don't process Alt+* combinations. return false; } if (event.getCommandKey() || event.getKeySignalType() != SignalEvent.KeySignalType.INPUT) { // Don't insert any Action+* / non-input combinations as text. return false; } InputController input = this.getScheme().getInputController(); SelectionModel selection = input.getSelection(); int column = selection.getCursorColumn(); if (KeyCodeMap.isPrintable(letter)) { String text = Character.toString((char) letter); // insert a single character input.getEditorDocumentMutator().insertText(selection.getCursorLine(), selection.getCursorLineNumber(), column, text); return true; } // let it fall through return false; } /** * Insert more than one character directly into the document */ @Override public boolean onDefaultPaste(SignalEvent event, String text) { SelectionModel selection = this.getScheme().getInputController().getSelection(); int column = selection.getCursorColumn(); getScheme().getInputController().getEditorDocumentMutator() .insertText(selection.getCursorLine(), selection.getCursorLineNumber(), column, text); return true; } }; /* * ACTION+S - prevent the default browser behavior because the document is * saved automatically. */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 's') { @Override public boolean event(InputScheme scheme, SignalEvent event) { // prevent ACTION+S return true; } }); /** * ACTION+A - select all text in the current document. * * @see SelectionModel#selectAll() */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'a') { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().getSelection().selectAll(); return true; } }); defaultMode.bindAction(CommonActions.GOTO_DEFINITION, ModifierKeys.ACTION, 'b'); defaultMode.bindAction(CommonActions.GOTO_SOURCE, ModifierKeys.NONE, KeyCodeMap.F4); defaultMode.bindAction(CommonActions.SPLIT_LINE, ModifierKeys.ACTION, KeyCodeMap.ENTER); defaultMode.bindAction(CommonActions.START_NEW_LINE, ModifierKeys.SHIFT, KeyCodeMap.ENTER); // Single / multi-line comment / uncomment. defaultMode.bindAction(CommonActions.TOGGLE_COMMENT, ModifierKeys.ACTION, KeyboardEvent.KeyCode.SLASH); // Multi-line indenting and dedenting. /** * TAB - add a tab at the current position, or at the beginning of each line * if there is a selection. * * @see InputController#indentSelection() */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.TAB) { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().handleTab(); return true; } }); /** * SHIFT+TAB - remove a tab from the beginning of each line in the * selection. * * @see InputController#dedentSelection() */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.SHIFT, KeyCodeMap.TAB) { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().dedentSelection(); return true; } }); /** * BACKSPACE - native behavior * * @see InputController#deleteCharacter(boolean) */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.BACKSPACE) { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().deleteCharacter(false); return true; } }); /** * SHIFT+BACKSPACE - native behavior * * @see InputController#deleteCharacter(boolean) */ // This is common due to people backspacing while typing uppercase chars defaultMode.addShortcut( new EventShortcut(ModifierKeys.SHIFT, KeyCodeMap.BACKSPACE) { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().deleteCharacter(false); return true; } }); /** * ACTION+BACKSPACE - delete previous word */ defaultMode.addShortcut(new EventShortcut(wordGrainModifier(), KeyCodeMap.BACKSPACE) { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().deleteWord(false); return true; } }); /** * DELETE - native behavior * * @see InputController#deleteCharacter(boolean) */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.DELETE) { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().deleteCharacter(true); return true; } }); /** * ESC - Broadcasts clear message */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.NONE, KeyCodeMap.ESC) { @Override public boolean event(InputScheme scheme, SignalEvent event) { // TODO: Make this broadcast event. return true; } }); /** * ACTION+DELETE - delete next word */ defaultMode.addShortcut(new EventShortcut(wordGrainModifier(), KeyCodeMap.DELETE) { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().deleteWord(true); return true; } }); /** * UNDO (ACTION+Z) - undo the most recent document action * * @see Editor#undo() */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'z') { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().getEditor().undo(); return true; } }); /** * REDO (ACTION+Y) - redo the last undone document action * * @see Editor#redo() */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'y') { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme.getInputController().getEditor().redo(); return true; } }); /** * Find Next (ACTION+G) - Goto next match */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'g') { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme .getInputController() .getEditor() .getSearchModel() .getMatchManager() .selectNextMatch(); return true; } }); /** * Find Previous (ACTION+SHIFT+G) - Goto previous match */ defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'G') { @Override public boolean event(InputScheme scheme, SignalEvent event) { scheme .getInputController() .getEditor() .getSearchModel() .getMatchManager() .selectPreviousMatch(); return true; } }); /** * ACTION+ALT+V - Switch from Default to Vim Scheme * * TODO: Removed VIM keybinding access until we're ready to launch * it */ if (false) { // Disabled due to prioritization defaultMode .addShortcut(new EventShortcut(ModifierKeys.ACTION | ModifierKeys.ALT, 'v') { @Override public boolean event(InputScheme scheme, SignalEvent event) { getInputController().setActiveInputScheme(getInputController().vimScheme); return true; } }); } if (ENABLE_DEBUG_SPACER_KEYS) { final SortedList<Spacer> spacers = new SortedList<Spacer>(new Spacer.Comparator()); final Spacer.OneWaySpacerComparator spacerFinder = new Spacer.OneWaySpacerComparator(); defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'i') { @Override public boolean event(InputScheme scheme, SignalEvent event) { final Editor editor = scheme.getInputController().getEditor(); spacers.add(editor.getBuffer().addSpacer( new LineInfo(editor.getSelection().getCursorLine(), editor.getSelection() .getCursorLineNumber()), new Random().nextInt(500) + 1)); return true; } }); defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'd') { @Override public boolean event(InputScheme scheme, SignalEvent event) { final Editor editor = scheme.getInputController().getEditor(); spacerFinder.setValue(editor.getSelection().getCursorLineNumber()); int spacerIndex = spacers.findInsertionIndex(spacerFinder, false); if (spacerIndex >= 0) { editor.getBuffer().removeSpacer(spacers.get(spacerIndex)); spacers.remove(spacerIndex); } return true; } }); defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'u') { @Override public boolean event(InputScheme scheme, SignalEvent event) { final Editor editor = scheme.getInputController().getEditor(); spacerFinder.setValue(editor.getSelection().getCursorLineNumber()); int spacerIndex = spacers.findInsertionIndex(spacerFinder, false); if (spacerIndex >= 0) { // spacers.get(spacerIndex).setHeight(new Random().nextInt(500)+1); } return true; } }); } if (ENABLE_ANIMATION_CONTROL_KEYS) { defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'e') { @Override public boolean event(InputScheme scheme, SignalEvent event) { final Editor editor = scheme.getInputController().getEditor(); editor.setAnimationEnabled(true); return true; } }); defaultMode.addShortcut(new EventShortcut(ModifierKeys.ACTION, 'd') { @Override public boolean event(InputScheme scheme, SignalEvent event) { final Editor editor = scheme.getInputController().getEditor(); editor.setAnimationEnabled(false); return true; } }); } addMode(1, defaultMode); } private static int wordGrainModifier() { return UserAgent.isMac() ? ModifierKeys.ALT : ModifierKeys.ACTION; } @Override public void setup() { switchMode(1); } }