/* * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.apple.laf; import java.awt.event.ActionEvent; import java.util.*; import javax.swing.*; import javax.swing.UIDefaults.LazyValue; import javax.swing.text.*; import javax.swing.text.DefaultEditorKit.DefaultKeyTypedAction; import com.apple.laf.AquaUtils.RecyclableSingleton; import com.apple.laf.AquaUtils.RecyclableSingletonFromDefaultConstructor; public class AquaKeyBindings { static final RecyclableSingleton instance = new RecyclableSingletonFromDefaultConstructor(AquaKeyBindings.class); static AquaKeyBindings instance() { return instance.get(); } final DefaultKeyTypedAction defaultKeyTypedAction = new DefaultKeyTypedAction(); void setDefaultAction(final String keymapName) { final javax.swing.text.Keymap map = JTextComponent.getKeymap(keymapName); map.setDefaultAction(defaultKeyTypedAction); } static final String upMultilineAction = "aqua-move-up"; static final String downMultilineAction = "aqua-move-down"; static final String pageUpMultiline = "aqua-page-up"; static final String pageDownMultiline = "aqua-page-down"; final String[] commonTextEditorBindings = { "ENTER", JTextField.notifyAction, "COPY", DefaultEditorKit.copyAction, "CUT", DefaultEditorKit.cutAction, "PASTE", DefaultEditorKit.pasteAction, "meta A", DefaultEditorKit.selectAllAction, "meta C", DefaultEditorKit.copyAction, "meta V", DefaultEditorKit.pasteAction, "meta X", DefaultEditorKit.cutAction, "meta BACK_SLASH", "unselect", "DELETE", DefaultEditorKit.deleteNextCharAction, "alt DELETE", "delete-next-word", "BACK_SPACE", DefaultEditorKit.deletePrevCharAction, "alt BACK_SPACE", "delete-previous-word", "LEFT", DefaultEditorKit.backwardAction, "KP_LEFT", DefaultEditorKit.backwardAction, "RIGHT", DefaultEditorKit.forwardAction, "KP_RIGHT", DefaultEditorKit.forwardAction, "shift LEFT", DefaultEditorKit.selectionBackwardAction, "shift KP_LEFT", DefaultEditorKit.selectionBackwardAction, "shift RIGHT", DefaultEditorKit.selectionForwardAction, "shift KP_RIGHT", DefaultEditorKit.selectionForwardAction, "meta LEFT", DefaultEditorKit.beginLineAction, "meta KP_LEFT", DefaultEditorKit.beginLineAction, "meta RIGHT", DefaultEditorKit.endLineAction, "meta KP_RIGHT", DefaultEditorKit.endLineAction, "shift meta LEFT", DefaultEditorKit.selectionBeginLineAction, "shift meta KP_LEFT", DefaultEditorKit.selectionBeginLineAction, "shift meta RIGHT", DefaultEditorKit.selectionEndLineAction, "shift meta KP_RIGHT", DefaultEditorKit.selectionEndLineAction, "alt LEFT", DefaultEditorKit.previousWordAction, "alt KP_LEFT", DefaultEditorKit.previousWordAction, "alt RIGHT", DefaultEditorKit.nextWordAction, "alt KP_RIGHT", DefaultEditorKit.nextWordAction, "shift alt LEFT", DefaultEditorKit.selectionPreviousWordAction, "shift alt KP_LEFT", DefaultEditorKit.selectionPreviousWordAction, "shift alt RIGHT", DefaultEditorKit.selectionNextWordAction, "shift alt KP_RIGHT", DefaultEditorKit.selectionNextWordAction, "control A", DefaultEditorKit.beginLineAction, "control B", DefaultEditorKit.backwardAction, "control D", DefaultEditorKit.deleteNextCharAction, "control E", DefaultEditorKit.endLineAction, "control F", DefaultEditorKit.forwardAction, "control H", DefaultEditorKit.deletePrevCharAction, "control W", "delete-previous-word", "control shift O", "toggle-componentOrientation", "END", DefaultEditorKit.endAction, "HOME", DefaultEditorKit.beginAction, "shift END", DefaultEditorKit.selectionEndAction, "shift HOME", DefaultEditorKit.selectionBeginAction, "PAGE_DOWN", pageDownMultiline, "PAGE_UP", pageUpMultiline, "shift PAGE_DOWN", "selection-page-down", "shift PAGE_UP", "selection-page-up", "meta shift PAGE_DOWN", "selection-page-right", "meta shift PAGE_UP", "selection-page-left", "meta DOWN", DefaultEditorKit.endAction, "meta KP_DOWN", DefaultEditorKit.endAction, "meta UP", DefaultEditorKit.beginAction, "meta KP_UP", DefaultEditorKit.beginAction, "shift meta DOWN", DefaultEditorKit.selectionEndAction, "shift meta KP_DOWN", DefaultEditorKit.selectionEndAction, "shift meta UP", DefaultEditorKit.selectionBeginAction, "shift meta KP_UP", DefaultEditorKit.selectionBeginAction, }; LateBoundInputMap getTextFieldInputMap() { return new LateBoundInputMap(new SimpleBinding(commonTextEditorBindings), new SimpleBinding(new String[] { "DOWN", DefaultEditorKit.endLineAction, "KP_DOWN", DefaultEditorKit.endLineAction, "UP", DefaultEditorKit.beginLineAction, "KP_UP", DefaultEditorKit.beginLineAction, "shift DOWN", DefaultEditorKit.selectionEndLineAction, "shift KP_DOWN", DefaultEditorKit.selectionEndLineAction, "shift UP", DefaultEditorKit.selectionBeginLineAction, "shift KP_UP", DefaultEditorKit.selectionBeginLineAction, "control P", DefaultEditorKit.beginAction, "control N", DefaultEditorKit.endAction, "control V", DefaultEditorKit.endAction, })); } LateBoundInputMap getPasswordFieldInputMap() { return new LateBoundInputMap(new SimpleBinding(getTextFieldInputMap().getBindings()), // nullify all the bindings that may discover space characters in the text new SimpleBinding(new String[] { "alt LEFT", null, "alt KP_LEFT", null, "alt RIGHT", null, "alt KP_RIGHT", null, "shift alt LEFT", null, "shift alt KP_LEFT", null, "shift alt RIGHT", null, "shift alt KP_RIGHT", null, })); } LateBoundInputMap getMultiLineTextInputMap() { return new LateBoundInputMap(new SimpleBinding(commonTextEditorBindings), new SimpleBinding(new String[] { "ENTER", DefaultEditorKit.insertBreakAction, "DOWN", downMultilineAction, "KP_DOWN", downMultilineAction, "UP", upMultilineAction, "KP_UP", upMultilineAction, "shift DOWN", DefaultEditorKit.selectionDownAction, "shift KP_DOWN", DefaultEditorKit.selectionDownAction, "shift UP", DefaultEditorKit.selectionUpAction, "shift KP_UP", DefaultEditorKit.selectionUpAction, "alt shift DOWN", DefaultEditorKit.selectionEndParagraphAction, "alt shift KP_DOWN", DefaultEditorKit.selectionEndParagraphAction, "alt shift UP", DefaultEditorKit.selectionBeginParagraphAction, "alt shift KP_UP", DefaultEditorKit.selectionBeginParagraphAction, "control P", DefaultEditorKit.upAction, "control N", DefaultEditorKit.downAction, "control V", pageDownMultiline, "TAB", DefaultEditorKit.insertTabAction, "meta SPACE", "activate-link-action", "meta T", "next-link-action", "meta shift T", "previous-link-action", "END", DefaultEditorKit.endAction, "HOME", DefaultEditorKit.beginAction, "shift END", DefaultEditorKit.selectionEndAction, "shift HOME", DefaultEditorKit.selectionBeginAction, "PAGE_DOWN", pageDownMultiline, "PAGE_UP", pageUpMultiline, "shift PAGE_DOWN", "selection-page-down", "shift PAGE_UP", "selection-page-up", "meta shift PAGE_DOWN", "selection-page-right", "meta shift PAGE_UP", "selection-page-left", })); } LateBoundInputMap getFormattedTextFieldInputMap() { return new LateBoundInputMap(getTextFieldInputMap(), new SimpleBinding(new String[] { "UP", "increment", "KP_UP", "increment", "DOWN", "decrement", "KP_DOWN", "decrement", "ESCAPE", "reset-field-edit", })); } LateBoundInputMap getComboBoxInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "ESCAPE", "aquaHidePopup", "PAGE_UP", "aquaSelectPageUp", "PAGE_DOWN", "aquaSelectPageDown", "HOME", "aquaSelectHome", "END", "aquaSelectEnd", "ENTER", "aquaEnterPressed", "UP", "aquaSelectPrevious", "KP_UP", "aquaSelectPrevious", "DOWN", "aquaSelectNext", "KP_DOWN", "aquaSelectNext", "SPACE", "aquaSpacePressed" // "spacePopup" })); } LateBoundInputMap getListInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "meta C", "copy", "meta V", "paste", "meta X", "cut", "COPY", "copy", "PASTE", "paste", "CUT", "cut", "UP", "selectPreviousRow", "KP_UP", "selectPreviousRow", "shift UP", "selectPreviousRowExtendSelection", "shift KP_UP", "selectPreviousRowExtendSelection", "DOWN", "selectNextRow", "KP_DOWN", "selectNextRow", "shift DOWN", "selectNextRowExtendSelection", "shift KP_DOWN", "selectNextRowExtendSelection", "LEFT", "selectPreviousColumn", "KP_LEFT", "selectPreviousColumn", "shift LEFT", "selectPreviousColumnExtendSelection", "shift KP_LEFT", "selectPreviousColumnExtendSelection", "RIGHT", "selectNextColumn", "KP_RIGHT", "selectNextColumn", "shift RIGHT", "selectNextColumnExtendSelection", "shift KP_RIGHT", "selectNextColumnExtendSelection", "meta A", "selectAll", // aquaHome and aquaEnd are new actions that just move the view so the first or last item is visible. "HOME", "aquaHome", "shift HOME", "selectFirstRowExtendSelection", "END", "aquaEnd", "shift END", "selectLastRowExtendSelection", // Unmodified PAGE_UP and PAGE_DOWN are handled by their scroll pane, if any. "shift PAGE_UP", "scrollUpExtendSelection", "shift PAGE_DOWN", "scrollDownExtendSelection" })); } LateBoundInputMap getScrollBarInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "RIGHT", "positiveUnitIncrement", "KP_RIGHT", "positiveUnitIncrement", "DOWN", "positiveUnitIncrement", "KP_DOWN", "positiveUnitIncrement", "PAGE_DOWN", "positiveBlockIncrement", "LEFT", "negativeUnitIncrement", "KP_LEFT", "negativeUnitIncrement", "UP", "negativeUnitIncrement", "KP_UP", "negativeUnitIncrement", "PAGE_UP", "negativeBlockIncrement", "HOME", "minScroll", "END", "maxScroll" })); } LateBoundInputMap getScrollBarRightToLeftInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "RIGHT", "negativeUnitIncrement", "KP_RIGHT", "negativeUnitIncrement", "LEFT", "positiveUnitIncrement", "KP_LEFT", "positiveUnitIncrement" })); } LateBoundInputMap getScrollPaneInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "RIGHT", "unitScrollRight", "KP_RIGHT", "unitScrollRight", "DOWN", "unitScrollDown", "KP_DOWN", "unitScrollDown", "LEFT", "unitScrollLeft", "KP_LEFT", "unitScrollLeft", "UP", "unitScrollUp", "KP_UP", "unitScrollUp", "PAGE_UP", "scrollUp", "PAGE_DOWN", "scrollDown", "HOME", "scrollHome", "END", "scrollEnd" })); } LateBoundInputMap getSliderInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "RIGHT", "positiveUnitIncrement", "KP_RIGHT", "positiveUnitIncrement", "DOWN", "negativeUnitIncrement", "KP_DOWN", "negativeUnitIncrement", "PAGE_DOWN", "negativeBlockIncrement", "LEFT", "negativeUnitIncrement", "KP_LEFT", "negativeUnitIncrement", "UP", "positiveUnitIncrement", "KP_UP", "positiveUnitIncrement", "PAGE_UP", "positiveBlockIncrement", "HOME", "minScroll", "END", "maxScroll" })); } LateBoundInputMap getSliderRightToLeftInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "RIGHT", "negativeUnitIncrement", "KP_RIGHT", "negativeUnitIncrement", "LEFT", "positiveUnitIncrement", "KP_LEFT", "positiveUnitIncrement" })); } LateBoundInputMap getSpinnerInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "UP", "increment", "KP_UP", "increment", "DOWN", "decrement", "KP_DOWN", "decrement" })); } LateBoundInputMap getTableInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "meta C", "copy", "meta V", "paste", "meta X", "cut", "COPY", "copy", "PASTE", "paste", "CUT", "cut", "RIGHT", "selectNextColumn", "KP_RIGHT", "selectNextColumn", "LEFT", "selectPreviousColumn", "KP_LEFT", "selectPreviousColumn", "DOWN", "selectNextRow", "KP_DOWN", "selectNextRow", "UP", "selectPreviousRow", "KP_UP", "selectPreviousRow", "shift RIGHT", "selectNextColumnExtendSelection", "shift KP_RIGHT", "selectNextColumnExtendSelection", "shift LEFT", "selectPreviousColumnExtendSelection", "shift KP_LEFT", "selectPreviousColumnExtendSelection", "shift DOWN", "selectNextRowExtendSelection", "shift KP_DOWN", "selectNextRowExtendSelection", "shift UP", "selectPreviousRowExtendSelection", "shift KP_UP", "selectPreviousRowExtendSelection", "PAGE_UP", "scrollUpChangeSelection", "PAGE_DOWN", "scrollDownChangeSelection", "HOME", "selectFirstColumn", "END", "selectLastColumn", "shift PAGE_UP", "scrollUpExtendSelection", "shift PAGE_DOWN", "scrollDownExtendSelection", "shift HOME", "selectFirstColumnExtendSelection", "shift END", "selectLastColumnExtendSelection", "TAB", "selectNextColumnCell", "shift TAB", "selectPreviousColumnCell", "meta A", "selectAll", "ESCAPE", "cancel", "ENTER", "selectNextRowCell", "shift ENTER", "selectPreviousRowCell", "alt TAB", "focusHeader", "alt shift TAB", "focusHeader" })); } LateBoundInputMap getTableRightToLeftInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "RIGHT", "selectPreviousColumn", "KP_RIGHT", "selectPreviousColumn", "LEFT", "selectNextColumn", "KP_LEFT", "selectNextColumn", "shift RIGHT", "selectPreviousColumnExtendSelection", "shift KP_RIGHT", "selectPreviousColumnExtendSelection", "shift LEFT", "selectNextColumnExtendSelection", "shift KP_LEFT", "selectNextColumnExtendSelection", "ctrl PAGE_UP", "scrollRightChangeSelection", "ctrl PAGE_DOWN", "scrollLeftChangeSelection", "ctrl shift PAGE_UP", "scrollRightExtendSelection", "ctrl shift PAGE_DOWN", "scrollLeftExtendSelection" })); } LateBoundInputMap getTreeInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "meta C", "copy", "meta V", "paste", "meta X", "cut", "COPY", "copy", "PASTE", "paste", "CUT", "cut", "UP", "selectPrevious", "KP_UP", "selectPrevious", "shift UP", "selectPreviousExtendSelection", "shift KP_UP", "selectPreviousExtendSelection", "DOWN", "selectNext", "KP_DOWN", "selectNext", "shift DOWN", "selectNextExtendSelection", "shift KP_DOWN", "selectNextExtendSelection", "RIGHT", "aquaExpandNode", "KP_RIGHT", "aquaExpandNode", "LEFT", "aquaCollapseNode", "KP_LEFT", "aquaCollapseNode", "shift RIGHT", "aquaExpandNode", "shift KP_RIGHT", "aquaExpandNode", "shift LEFT", "aquaCollapseNode", "shift KP_LEFT", "aquaCollapseNode", "ctrl LEFT", "aquaCollapseNode", "ctrl KP_LEFT", "aquaCollapseNode", "ctrl RIGHT", "aquaExpandNode", "ctrl KP_RIGHT", "aquaExpandNode", "alt RIGHT", "aquaFullyExpandNode", "alt KP_RIGHT", "aquaFullyExpandNode", "alt LEFT", "aquaFullyCollapseNode", "alt KP_LEFT", "aquaFullyCollapseNode", "meta A", "selectAll", "RETURN", "startEditing" })); } LateBoundInputMap getTreeRightToLeftInputMap() { return new LateBoundInputMap(new SimpleBinding(new String[] { "RIGHT", "aquaCollapseNode", "KP_RIGHT", "aquaCollapseNode", "LEFT", "aquaExpandNode", "KP_LEFT", "aquaExpandNode", "shift RIGHT", "aquaCollapseNode", "shift KP_RIGHT", "aquaCollapseNode", "shift LEFT", "aquaExpandNode", "shift KP_LEFT", "aquaExpandNode", "ctrl LEFT", "aquaExpandNode", "ctrl KP_LEFT", "aquaExpandNode", "ctrl RIGHT", "aquaCollapseNode", "ctrl KP_RIGHT", "aquaCollapseNode" })); } // common interface between a string array, and a dynamic provider of string arrays ;-) interface BindingsProvider { public String[] getBindings(); } // wraps basic string arrays static class SimpleBinding implements BindingsProvider { final String[] bindings; public SimpleBinding(final String[] bindings) { this.bindings = bindings; } public String[] getBindings() { return bindings; } } // patches all providers together at the moment the UIManager needs the real InputMap static class LateBoundInputMap implements LazyValue, BindingsProvider { private final BindingsProvider[] providerList; private String[] mergedBindings; public LateBoundInputMap(final BindingsProvider ... providerList) { this.providerList = providerList; } public Object createValue(final UIDefaults table) { return LookAndFeel.makeInputMap(getBindings()); } public String[] getBindings() { if (mergedBindings != null) return mergedBindings; final String[][] bindingsList = new String[providerList.length][]; int size = 0; for (int i = 0; i < providerList.length; i++) { bindingsList[i] = providerList[i].getBindings(); size += bindingsList[i].length; } if (bindingsList.length == 1) { return mergedBindings = bindingsList[0]; } final ArrayList unifiedList = new ArrayList(size); Collections.addAll(unifiedList, bindingsList[0]); // System.arrayCopy() the first set for (int i = 1; i < providerList.length; i++) { mergeBindings(unifiedList, bindingsList[i]); } return mergedBindings = unifiedList.toArray(new String[unifiedList.size()]); } static void mergeBindings(final ArrayList unifiedList, final String[] overrides) { for (int i = 0; i < overrides.length; i+=2) { final String key = overrides[i]; final String value = overrides[i+1]; final int keyIndex = unifiedList.indexOf(key); if (keyIndex == -1) { unifiedList.add(key); unifiedList.add(value); } else { unifiedList.set(keyIndex, key); unifiedList.set(keyIndex + 1, value); } } } } void installAquaUpDownActions(final JTextComponent component) { final ActionMap actionMap = component.getActionMap(); actionMap.put(upMultilineAction, moveUpMultilineAction); actionMap.put(downMultilineAction, moveDownMultilineAction); actionMap.put(pageUpMultiline, pageUpMultilineAction); actionMap.put(pageDownMultiline, pageDownMultilineAction); } // extracted and adapted from DefaultEditorKit in 1.6 @SuppressWarnings("serial") // Superclass is not serializable across versions static abstract class DeleteWordAction extends TextAction { public DeleteWordAction(final String name) { super(name); } public void actionPerformed(final ActionEvent e) { if (e == null) return; final JTextComponent target = getTextComponent(e); if (target == null) return; if (!target.isEditable() || !target.isEnabled()) { UIManager.getLookAndFeel().provideErrorFeedback(target); return; } try { final int start = target.getSelectionStart(); final Element line = Utilities.getParagraphElement(target, start); final int end = getEnd(target, line, start); final int offs = Math.min(start, end); final int len = Math.abs(end - start); if (offs >= 0) { target.getDocument().remove(offs, len); return; } } catch (final BadLocationException ignore) {} UIManager.getLookAndFeel().provideErrorFeedback(target); } abstract int getEnd(final JTextComponent target, final Element line, final int start) throws BadLocationException; } final TextAction moveUpMultilineAction = new AquaMultilineAction(upMultilineAction, DefaultEditorKit.upAction, DefaultEditorKit.beginAction); final TextAction moveDownMultilineAction = new AquaMultilineAction(downMultilineAction, DefaultEditorKit.downAction, DefaultEditorKit.endAction); final TextAction pageUpMultilineAction = new AquaMultilineAction(pageUpMultiline, DefaultEditorKit.pageUpAction, DefaultEditorKit.beginAction); final TextAction pageDownMultilineAction = new AquaMultilineAction(pageDownMultiline, DefaultEditorKit.pageDownAction, DefaultEditorKit.endAction); @SuppressWarnings("serial") // Superclass is not serializable across versions static class AquaMultilineAction extends TextAction { final String targetActionName; final String proxyActionName; public AquaMultilineAction(final String actionName, final String targetActionName, final String proxyActionName) { super(actionName); this.targetActionName = targetActionName; this.proxyActionName = proxyActionName; } public void actionPerformed(final ActionEvent e) { final JTextComponent c = getTextComponent(e); final ActionMap actionMap = c.getActionMap(); final Action targetAction = actionMap.get(targetActionName); final int startPosition = c.getCaretPosition(); targetAction.actionPerformed(e); if (startPosition != c.getCaretPosition()) return; final Action proxyAction = actionMap.get(proxyActionName); proxyAction.actionPerformed(e); } } }