/*
 * 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 {
    private static final RecyclableSingleton<AquaKeyBindings> instance = new RecyclableSingletonFromDefaultConstructor<AquaKeyBindings>(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", "enterPressed",
            "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<String> unifiedList = new ArrayList<String>(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<String> 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
    abstract static 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);
        }
    }
}