<NME> GenericStyledAreaBehavior.java
<BEF> package org.fxmisc.richtext;

import static java.lang.Character.*;
import static javafx.scene.input.KeyCode.*;
import static javafx.scene.input.KeyCombination.*;
import static org.fxmisc.richtext.model.TwoDimensional.Bias.*;
import static org.fxmisc.wellbehaved.event.EventPattern.*;
import static org.fxmisc.wellbehaved.event.template.InputMapTemplate.*;
import static org.reactfx.EventStreams.*;

import java.util.function.Predicate;

import javafx.event.Event;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.IndexRange;
import javafx.scene.input.ContextMenuEvent;
import javafx.scene.input.KeyCharacterCombination;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;

import org.fxmisc.richtext.NavigationActions.SelectionPolicy;
import org.fxmisc.richtext.model.TwoDimensional.Position;
import org.fxmisc.wellbehaved.event.EventPattern;
import org.fxmisc.wellbehaved.event.InputHandler.Result;
import org.fxmisc.wellbehaved.event.template.InputMapTemplate;
import org.reactfx.EventStream;
import org.reactfx.value.Val;
import org.reactfx.value.Var;

/**
 * Controller for GenericStyledArea.
 */
class GenericStyledAreaBehavior {

    private static final boolean isMac;
    private static final boolean isWindows;
    static {
        String os = System.getProperty("os.name");
        isMac = os.startsWith("Mac");
        isWindows = os.startsWith("Windows");
    }

    private static final InputMapTemplate<GenericStyledAreaBehavior, ? super Event> EVENT_TEMPLATE;
    private static final Predicate<KeyEvent> controlKeysFilter;

    static {
        SelectionPolicy selPolicy = isMac
                ? SelectionPolicy.EXTEND
                : SelectionPolicy.ADJUST;

        /*
         * KeyCodes are misinterpreted when using a different keyboard layout, for example:
         * on Dvorak: C results in KeyCode I, X -> B, and V -> . 
         * and on German layouts: Z and Y are reportedly switched
         * so then editing commands such as Ctrl+C, or CMD+Z are incorrectly processed.
         * KeyCharacterCombination however does keyboard translation before matching.
         * This resolves issue #799
         */
        KeyCharacterCombination SHORTCUT_A = new KeyCharacterCombination( "a", SHORTCUT_DOWN );
        KeyCharacterCombination SHORTCUT_C = new KeyCharacterCombination( "c", SHORTCUT_DOWN );
        KeyCharacterCombination SHORTCUT_V = new KeyCharacterCombination( "v", SHORTCUT_DOWN );
        KeyCharacterCombination SHORTCUT_X = new KeyCharacterCombination( "x", SHORTCUT_DOWN );
        KeyCharacterCombination SHORTCUT_Y = new KeyCharacterCombination( "y", SHORTCUT_DOWN );
        KeyCharacterCombination SHORTCUT_Z = new KeyCharacterCombination( "z", SHORTCUT_DOWN );
        KeyCharacterCombination SHORTCUT_SHIFT_Z = new KeyCharacterCombination( "z", SHORTCUT_DOWN, SHIFT_DOWN );

                // deletion
                consume(keyPressed(DELETE),                     GenericStyledAreaBehavior::deleteForward),
                consume(keyPressed(BACK_SPACE),                 GenericStyledAreaBehavior::deleteBackward),
                consume(keyPressed(DELETE,     SHORTCUT_DOWN),  GenericStyledAreaBehavior::deleteNextWord),
                consume(keyPressed(BACK_SPACE, SHORTCUT_DOWN),  GenericStyledAreaBehavior::deletePrevWord),
                // cut
                // cut
                consume(
                        anyOf(keyPressed(CUT),   keyPressed(SHORTCUT_X), keyPressed(DELETE, SHIFT_DOWN)),
                        (b, e) -> b.view.cut()),
                // paste
                consume(
                        anyOf(keyPressed(PASTE), keyPressed(SHORTCUT_V), keyPressed(INSERT, SHIFT_DOWN)),
                        (b, e) -> b.view.paste()),
                // tab & newline
                consume(keyPressed(ENTER), (b, e) -> b.view.replaceSelection("\n")),
                consume(keyPressed(TAB),   (b, e) -> b.view.replaceSelection("\t")),
                // undo/redo
                consume(keyPressed(SHORTCUT_Z), (b, e) -> b.view.undo()),
                consume(
                        anyOf(keyPressed(SHORTCUT_Y), keyPressed(SHORTCUT_SHIFT_Z)),
                        (b, e) -> b.view.redo()),
                // insert/overwrite
                consume(keyPressed(INSERT), GenericStyledAreaBehavior::toggelOverwriteMode)
        );
        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> edits = when(b -> b.view.isEditable(), editsBase);

        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> verticalNavigation = sequence(
                // vertical caret movement
                consume(
                        anyOf(keyPressed(UP), keyPressed(KP_UP)),
                        (b, e) -> b.prevLine(SelectionPolicy.CLEAR)),
                consume(
                        anyOf(keyPressed(DOWN), keyPressed(KP_DOWN)),
                        (b, e) -> b.nextLine(SelectionPolicy.CLEAR)),
                consume(keyPressed(PAGE_UP),    (b, e) -> b.view.prevPage(SelectionPolicy.CLEAR)),
                consume(keyPressed(PAGE_DOWN),  (b, e) -> b.view.nextPage(SelectionPolicy.CLEAR)),
                // vertical selection
                consume(
                        anyOf(keyPressed(UP,   SHIFT_DOWN), keyPressed(KP_UP, SHIFT_DOWN)),
                        (b, e) -> b.prevLine(SelectionPolicy.ADJUST)),
                consume(
                        anyOf(keyPressed(DOWN, SHIFT_DOWN), keyPressed(KP_DOWN, SHIFT_DOWN)),
                        (b, e) -> b.nextLine(SelectionPolicy.ADJUST)),
                consume(keyPressed(PAGE_UP,   SHIFT_DOWN),  (b, e) -> b.view.prevPage(SelectionPolicy.ADJUST)),
                consume(keyPressed(PAGE_DOWN, SHIFT_DOWN),  (b, e) -> b.view.nextPage(SelectionPolicy.ADJUST))
        );

        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> otherNavigation = sequence(
                // caret movement
                consume(anyOf(keyPressed(RIGHT), keyPressed(KP_RIGHT)), GenericStyledAreaBehavior::right),
                consume(anyOf(keyPressed(LEFT),  keyPressed(KP_LEFT)),  GenericStyledAreaBehavior::left),
                consume(keyPressed(HOME), (b, e) -> b.view.lineStart(SelectionPolicy.CLEAR)),
                consume(keyPressed(END),  (b, e) -> b.view.lineEnd(SelectionPolicy.CLEAR)),
                consume(
                        anyOf(
                                keyPressed(RIGHT,    SHORTCUT_DOWN),
                                keyPressed(KP_RIGHT, SHORTCUT_DOWN)
                        ), (b, e) -> b.skipToNextWord(SelectionPolicy.CLEAR)),
                consume(
                        anyOf(
                                keyPressed(LEFT,     SHORTCUT_DOWN),
                                keyPressed(KP_LEFT,  SHORTCUT_DOWN)
                        ), (b, e) -> b.skipToPrevWord(SelectionPolicy.CLEAR)),
                consume(keyPressed(HOME, SHORTCUT_DOWN), (b, e) -> b.view.start(SelectionPolicy.CLEAR)),
                consume(keyPressed(END,  SHORTCUT_DOWN), (b, e) -> b.view.end(SelectionPolicy.CLEAR)),
                // selection
                consume(
                        anyOf(
                                keyPressed(RIGHT,    SHIFT_DOWN),
                                keyPressed(KP_RIGHT, SHIFT_DOWN)
                        ), GenericStyledAreaBehavior::selectRight),
                consume(
                        anyOf(
                                keyPressed(LEFT,     SHIFT_DOWN),
                                keyPressed(KP_LEFT,  SHIFT_DOWN)
                        ), GenericStyledAreaBehavior::selectLeft),
                consume(keyPressed(HOME, SHIFT_DOWN),                (b, e) -> b.view.lineStart(selPolicy)),
                consume(keyPressed(END,  SHIFT_DOWN),                (b, e) -> b.view.lineEnd(selPolicy)),
                consume(keyPressed(HOME, SHIFT_DOWN, SHORTCUT_DOWN), (b, e) -> b.view.start(selPolicy)),
                consume(keyPressed(END,  SHIFT_DOWN, SHORTCUT_DOWN), (b, e) -> b.view.end(selPolicy)),
                consume(
                        anyOf(
                                keyPressed(RIGHT,    SHIFT_DOWN, SHORTCUT_DOWN),
                                keyPressed(KP_RIGHT, SHIFT_DOWN, SHORTCUT_DOWN)
                        ), (b, e) -> b.skipToNextWord(selPolicy)),
                consume(
                        anyOf(
                                keyPressed(LEFT,     SHIFT_DOWN, SHORTCUT_DOWN),
                                keyPressed(KP_LEFT,  SHIFT_DOWN, SHORTCUT_DOWN)
                        ), (b, e) -> b.skipToPrevWord(selPolicy)),
                consume(keyPressed(SHORTCUT_A), (b, e) -> b.view.selectAll())
        );

        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> copyAction = consume(
                anyOf(
                        keyPressed(COPY),
                        keyPressed(SHORTCUT_C),
                        keyPressed(INSERT, SHORTCUT_DOWN)
                ), (b, e) -> b.view.copy()
        );

        controlKeysFilter = e -> {
            if (isWindows) {
                //Windows input. ALT + CONTROL accelerators are the same as ALT GR accelerators.
                //If ALT + CONTROL are pressed and the given character is valid then print the character.
                //Else, don't consume the event. This change allows Windows users to use accelerators and
                //printing special characters at the same time.
                // (For example: ALT + CONTROL + E prints the euro symbol in the spanish keyboard while ALT + CONTROL + L has assigned an accelerator.)
                //Note that this is how several IDEs such JetBrains IDEs or Eclipse behave.
                if (e.isControlDown() && e.isAltDown() && !e.isMetaDown() && e.getCharacter().length() == 1
                	    && e.getCharacter().getBytes()[0] != 0) return true;
                
                return !e.isControlDown() && !e.isAltDown() && !e.isMetaDown();
            }
        	return !e.isControlDown() && !e.isMetaDown();
        };

        Predicate<KeyEvent> isChar = e ->
                e.getCode().isLetterKey() ||
                e.getCode().isDigitKey() ||
                e.getCode().isWhitespaceKey();

        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> charPressConsumer = consume(keyPressed().onlyIf(isChar.and(controlKeysFilter)));

        InputMapTemplate<GenericStyledAreaBehavior, ? super KeyEvent> keyPressedTemplate = edits
                .orElse(otherNavigation).ifConsumed((b, e) -> b.view.clearTargetCaretOffset())
                .orElse(verticalNavigation)
                .orElse(copyAction)
                .ifConsumed((b, e) -> b.view.requestFollowCaret())
                // no need to add 'ifConsumed' after charPress since
                // requestFollowCaret is called in keyTypedTemplate
                .orElse(charPressConsumer);

        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> keyTypedBase = consume(
                // character input
                EventPattern.keyTyped().onlyIf(controlKeysFilter.and(e -> isLegal(e.getCharacter()))),
                GenericStyledAreaBehavior::keyTyped
        ).ifConsumed((b, e) -> b.view.requestFollowCaret());
        InputMapTemplate<GenericStyledAreaBehavior, ? super KeyEvent> keyTypedTemplate = when(b -> b.view.isEditable(), keyTypedBase);

        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mousePressedTemplate = sequence(
                // ignore mouse pressed events if the view is disabled
                process(mousePressed(MouseButton.PRIMARY), (b, e) -> b.view.isDisabled() ? Result.IGNORE : Result.PROCEED),

                // hide context menu before any other handling
                process(
                        mousePressed(), (b, e) -> {
                            b.view.hideContextMenu();
                            return Result.PROCEED;
                        }
                ),
                consume(
                        mousePressed(MouseButton.PRIMARY).onlyIf(MouseEvent::isShiftDown),
                        GenericStyledAreaBehavior::handleShiftPress
                ),
                consume(
                        mousePressed(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 1),
                        GenericStyledAreaBehavior::handleFirstPrimaryPress
                ),
                consume(
                        mousePressed(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 2),
                        GenericStyledAreaBehavior::handleSecondPress
                ),
                consume(
                        mousePressed(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 3),
                        GenericStyledAreaBehavior::handleThirdPress
                )
        );

        Predicate<MouseEvent> primaryOnlyButton = e -> e.getButton() == MouseButton.PRIMARY && !e.isMiddleButtonDown() && !e.isSecondaryButtonDown();

        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mouseDragDetectedTemplate = consume(
                eventType(MouseEvent.DRAG_DETECTED).onlyIf(primaryOnlyButton),
                (b, e) -> b.handlePrimaryOnlyDragDetected()
        );

        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mouseDragTemplate = sequence(
                process(
                        mouseDragged().onlyIf(primaryOnlyButton),
                        GenericStyledAreaBehavior::processPrimaryOnlyMouseDragged
                ),
                consume(
                        mouseDragged(),
                        GenericStyledAreaBehavior::continueOrStopAutoScroll
                )
        );

        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mouseReleasedTemplate = sequence(
                process(
                        EventPattern.mouseReleased().onlyIf(primaryOnlyButton),
                        GenericStyledAreaBehavior::processMouseReleased
                ),
                consume(
                        mouseReleased(),
                        (b, e) -> b.autoscrollTo.setValue(null)  // stop auto scroll
                )
        );

        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mouseTemplate = sequence(
                mousePressedTemplate, mouseDragDetectedTemplate, mouseDragTemplate, mouseReleasedTemplate
        );

        InputMapTemplate<GenericStyledAreaBehavior, ? super ContextMenuEvent> contextMenuEventTemplate = consumeWhen(
                EventPattern.eventType(ContextMenuEvent.CONTEXT_MENU_REQUESTED),
                b -> !b.view.isDisabled(),
                GenericStyledAreaBehavior::showContextMenu
        );

        EVENT_TEMPLATE = sequence(mouseTemplate, keyPressedTemplate, keyTypedTemplate, contextMenuEventTemplate);
    }

    /**
     * Possible dragging states.
     */
    private enum DragState {
        /** No dragging is happening. */
        NO_DRAG,

        /** Mouse has been pressed inside of selected text, but drag has not been detected yet. */
        POTENTIAL_DRAG,

        /** Drag in progress. */
        DRAG,
    }

    /* ********************************************************************** *
     * Fields                                                                 *
     * ********************************************************************** */

    private final GenericStyledArea<?, ?, ?> view;

    /**
     * Indicates weather the area is in overwrite or insert mode.
     */
    private boolean overwriteMode = false;

    /**
     * Indicates whether an existing selection is being dragged by the user.
     */
    private DragState dragSelection = DragState.NO_DRAG;

    /**
     * Indicates whether a new selection is being made by the user.
     */
    private DragState dragNewSelection = DragState.NO_DRAG;

    private final Var<Point2D> autoscrollTo = Var.newSimpleVar(null);

    /* ********************************************************************** *
     * Constructors                                                           *
     * ********************************************************************** */

    GenericStyledAreaBehavior(GenericStyledArea<?, ?, ?> area) {
        this.view = area;

        InputMapTemplate.installFallback(EVENT_TEMPLATE, this, b -> b.view);

        // setup auto-scroll
        Val<Point2D> projection = Val.combine(
                autoscrollTo,
                area.layoutBoundsProperty(),
                GenericStyledAreaBehavior::project);
        Val<Point2D> distance = Val.combine(
                autoscrollTo,
                projection,
                Point2D::subtract);
        EventStream<Point2D> deltas = nonNullValuesOf(distance)
                .emitBothOnEach(animationFrames())
                .map(t -> t.map((ds, nanos) -> ds.multiply(nanos / 100_000_000.0)));
        valuesOf(autoscrollTo).flatMap(p -> p == null
                ? never() // automatically stops the scroll animation
                : deltas)
            .subscribe(ds -> {
                area.scrollBy(ds);
                projection.ifPresent(this::dragTo);
            });
    }

    /* ********************************************************************** *
     * Key handling implementation                                            *
     * ********************************************************************** */

    private void keyTyped(KeyEvent event) {
        String text = event.getCharacter();
        int n = text.length();

        if(n == 0) {
            return;
        }

        IndexRange range = view.getSelection();
        int start = range.getStart();
        int end = range.getEnd();

        if (overwriteMode && start == end) {
            end = Math.min(end+1, view.getLength());
        }

        view.replaceText(start, end, text);
    }

    private void toggelOverwriteMode(KeyEvent ignore) {
        overwriteMode = !overwriteMode;
    }

    private void deleteBackward(KeyEvent ignore) {
        IndexRange selection = view.getSelection();
        if(selection.getLength() == 0) {
            view.deletePreviousChar();
        } else {
            view.replaceSelection("");
        }
    }

    private void deleteForward(KeyEvent ignore) {
        IndexRange selection = view.getSelection();
        if(selection.getLength() == 0) {
            view.deleteNextChar();
        } else {
            view.replaceSelection("");
        }
    }

    private void left(KeyEvent ignore) {
        IndexRange sel = view.getSelection();
        if(sel.getLength() == 0) {
            view.previousChar(SelectionPolicy.CLEAR);
        } else {
            view.moveTo(sel.getStart(), SelectionPolicy.CLEAR);
        }
    }

    private void right(KeyEvent ignore) {
        IndexRange sel = view.getSelection();
        if(sel.getLength() == 0) {
            view.nextChar(SelectionPolicy.CLEAR);
        } else {
            view.moveTo(sel.getEnd(), SelectionPolicy.CLEAR);
        }
    }

    private void selectLeft(KeyEvent ignore) {
        view.previousChar(SelectionPolicy.ADJUST);
    }

    private void selectRight(KeyEvent ignore) {
        view.nextChar(SelectionPolicy.ADJUST);
    }

    private void deletePrevWord(KeyEvent ignore) {
        int end = view.getCaretPosition();

        if (end > 0) {
            view.wordBreaksBackwards(2, SelectionPolicy.CLEAR);
            int start = view.getCaretPosition();
            view.replaceText(start, end, "");
        }
    }

    private void deleteNextWord(KeyEvent ignore) {
        int start = view.getCaretPosition();

        if (start < view.getLength()) {
            view.wordBreaksForwards(2, SelectionPolicy.CLEAR);
            int end = view.getCaretPosition();
            view.replaceText(start, end, "");
        }
    }

    private void downLines(SelectionPolicy selectionPolicy, int nLines) {
        Position currentLine = view.currentLine();
        Position targetLine = currentLine.offsetBy(nLines, Forward).clamp();
        if(!currentLine.sameAs(targetLine)) {
            // compute new caret position
            CharacterHit hit = view.hit(view.getTargetCaretOffset(), targetLine);

            // update model
            view.moveTo(hit.getInsertionIndex(), selectionPolicy);
        }
    }

    private void prevLine(SelectionPolicy selectionPolicy) {
        downLines(selectionPolicy, -1);
    }

    private void nextLine(SelectionPolicy selectionPolicy) {
        downLines(selectionPolicy, 1);
    }

    private void skipToPrevWord(SelectionPolicy selectionPolicy) {
        int caretPos = view.getCaretPosition();

        // if (0 == caretPos), do nothing as can't move to the left anyway
        if (1 <= caretPos ) {
            boolean prevCharIsWhiteSpace = isWhitespace(view.getText(caretPos - 1, caretPos).charAt(0));
            view.wordBreaksBackwards(prevCharIsWhiteSpace ? 2 : 1, selectionPolicy);
        }
    }

    private void skipToNextWord(SelectionPolicy selectionPolicy) {
        int caretPos = view.getCaretPosition();
        int length = view.getLength();

        // if (caretPos == length), do nothing as can't move to the right anyway
        if (caretPos <= length - 1) {
            boolean nextCharIsWhiteSpace = isWhitespace(view.getText(caretPos, caretPos + 1).charAt(0));
            view.wordBreaksForwards(nextCharIsWhiteSpace ? 2 : 1, selectionPolicy);
        }
    }

    /* ********************************************************************** *
     * Mouse handling implementation                                          *
     * ********************************************************************** */

    private void showContextMenu(ContextMenuEvent e) {
        view.requestFocus();
        if ( view.isContextMenuPresent() ) {
            ContextMenu menu = view.getContextMenu();
            double x = e.getScreenX() + view.getContextMenuXOffset();
            double y = e.getScreenY() + view.getContextMenuYOffset();
            menu.show( view, x, y );
        }
    }

    private void handleShiftPress(MouseEvent e) {
        // ensure focus
        view.requestFocus();

        CharacterHit hit = view.hit(e.getX(), e.getY());

        // On Mac always extend selection,
        // switching anchor and caret if necessary.
        view.moveTo(hit.getInsertionIndex(), isMac ? SelectionPolicy.EXTEND : SelectionPolicy.ADJUST);
    }

    private void handleFirstPrimaryPress(MouseEvent e) {
        // ensure focus
        view.requestFocus();

        CharacterHit hit = view.hit(e.getX(), e.getY());

        view.clearTargetCaretOffset();
        IndexRange selection = view.getSelection();
        if(view.isEditable() &&
                selection.getLength() != 0 &&
                hit.getCharacterIndex().isPresent() &&
                hit.getCharacterIndex().getAsInt() >= selection.getStart() &&
                hit.getCharacterIndex().getAsInt() < selection.getEnd()) {
            // press inside selection
            dragSelection = DragState.POTENTIAL_DRAG;
            dragNewSelection = DragState.NO_DRAG;
        } else {
            dragSelection = DragState.NO_DRAG;
            dragNewSelection = DragState.NO_DRAG;
            view.getOnOutsideSelectionMousePressed().handle(e);
        }
    }

    private void handleSecondPress(MouseEvent e) {
        view.selectWord();
    }

    private void handleThirdPress(MouseEvent e) {
        view.selectParagraph();
    }

    private void handlePrimaryOnlyDragDetected() {
        if (dragSelection == DragState.POTENTIAL_DRAG) {
            dragSelection = DragState.DRAG;
        }
        else dragNewSelection = DragState.DRAG;
    }

    private Result processPrimaryOnlyMouseDragged(MouseEvent e) {
        Point2D p = new Point2D(e.getX(), e.getY());
        if(view.getLayoutBounds().contains(p)) {
            dragTo(p);
        }
        view.setAutoScrollOnDragDesired(true);
        // autoScrollTo will be set in "continueOrStopAutoScroll(MouseEvent)"
        return Result.PROCEED;
    }

    private void continueOrStopAutoScroll(MouseEvent e) {
        if (!view.isAutoScrollOnDragDesired()) {
            autoscrollTo.setValue(null); // stops auto-scroll
        }

        Point2D p = new Point2D(e.getX(), e.getY());
        if(view.getLayoutBounds().contains(p)) {
            autoscrollTo.setValue(null); // stops auto-scroll
        } else {
            autoscrollTo.setValue(p);    // starts auto-scroll
        }
    }

    private void dragTo(Point2D point) {
        if(dragSelection == DragState.DRAG ||
                dragSelection == DragState.POTENTIAL_DRAG) { // MOUSE_DRAGGED may arrive even before DRAG_DETECTED
            view.getOnSelectionDrag().accept(point);
        } else {
            view.getOnNewSelectionDrag().accept(point);
        }
    }

    private Result processMouseReleased(MouseEvent e) {
        if (view.isDisabled()) {
            return Result.IGNORE;
        }

        switch(dragSelection) {
            case POTENTIAL_DRAG:
                // selection was not dragged, but clicked
                view.getOnInsideSelectionMousePressReleased().handle(e);
            case DRAG:
                view.getOnSelectionDropped().handle(e);
                break;
            case NO_DRAG: if ( dragNewSelection == DragState.DRAG ) {
                view.getOnNewSelectionDragFinished().handle(e);
            }
        }
        dragNewSelection = DragState.NO_DRAG;
        dragSelection = DragState.NO_DRAG;

        return Result.PROCEED;
    }

    private static Point2D project(Point2D p, Bounds bounds) {
        double x = clamp(p.getX(), bounds.getMinX(), bounds.getMaxX());
        double y = clamp(p.getY(), bounds.getMinY(), bounds.getMaxY());
        return new Point2D(x, y);
    }

    private static double clamp(double x, double min, double max) {
        return Math.min(Math.max(x, min), max);
    }

    static boolean isControlKeyEvent(KeyEvent event) {
    	return controlKeysFilter.test(event);
    }

    private static boolean isLegal(String text) {
        int n = text.length();
        for(int i = 0; i < n; ++i) {
            if(Character.isISOControl(text.charAt(i))) {
                return false;
            }
        }
        return true;
    }
}

<MSG> Added shift backspace to delete backward
<DFF> @@ -70,6 +70,7 @@ class GenericStyledAreaBehavior {
                 // deletion
                 consume(keyPressed(DELETE),                     GenericStyledAreaBehavior::deleteForward),
                 consume(keyPressed(BACK_SPACE),                 GenericStyledAreaBehavior::deleteBackward),
+                consume(keyPressed(BACK_SPACE, SHIFT_DOWN),     GenericStyledAreaBehavior::deleteBackward),
                 consume(keyPressed(DELETE,     SHORTCUT_DOWN),  GenericStyledAreaBehavior::deleteNextWord),
                 consume(keyPressed(BACK_SPACE, SHORTCUT_DOWN),  GenericStyledAreaBehavior::deletePrevWord),
                 // cut

{'commit': 'diff --git a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledAreaBehavior.java b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledAreaBehavior.java\nindex 9cc1877..794f06e 100644\n--- a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledAreaBehavior.java\n+++ b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledAreaBehavior.java\n@@ -70,6 +70,7 @@ class GenericStyledAreaBehavior {\n                 // deletion\n                 consume(keyPressed(DELETE),                     GenericStyledAreaBehavior::deleteForward),\n                 consume(keyPressed(BACK_SPACE),                 GenericStyledAreaBehavior::deleteBackward),\n+                consume(keyPressed(BACK_SPACE, SHIFT_DOWN),     GenericStyledAreaBehavior::deleteBackward),\n                 consume(keyPressed(DELETE,     SHORTCUT_DOWN),  GenericStyledAreaBehavior::deleteNextWord),\n                 consume(keyPressed(BACK_SPACE, SHORTCUT_DOWN),  GenericStyledAreaBehavior::deletePrevWord),\n                 // cut\n', 'commit_subject': 'Added shift backspace to delete backward', 'repo_name': 'FXMisc/RichTextFX', 'committer': {'name': 'Jurgen', 'email': '278122b7034b19f83f57f369d5ffe677ffb7d24e@gmail.com', 'time_sec': '1571145171', 'tz_offset': '120', 'date': {'seconds': '1571145171'}}, 'commit_tree': '5769d0983afec3388c6215907787901bf1f71cf7', 'commit_message': 'Added shift backspace to delete backward', 'language_name': 'Shell', 'license': 'bsd-2-clause', 'hunks': '@@ -70,6 +70,7 @@ class GenericStyledAreaBehavior {\n                 // deletion\n                 consume(keyPressed(DELETE),                     GenericStyledAreaBehavior::deleteForward),\n                 consume(keyPressed(BACK_SPACE),                 GenericStyledAreaBehavior::deleteBackward),\n+                consume(keyPressed(BACK_SPACE, SHIFT_DOWN),     GenericStyledAreaBehavior::deleteBackward),\n                 consume(keyPressed(DELETE,     SHORTCUT_DOWN),  GenericStyledAreaBehavior::deleteNextWord),\n                 consume(keyPressed(BACK_SPACE, SHORTCUT_DOWN),  GenericStyledAreaBehavior::deletePrevWord),\n                 // cut\n', 'addition_count': 1, 'deletion_count': 0, 'src_file': 'a/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledAreaBehavior.java', 'tgt_file': 'b/richtextfx/src/main/java/org/fxmisc/richtext/GenericStyledAreaBehavior.java', 'file_extension': '.java', 'before_file': 'package org.fxmisc.richtext;\n\nimport static java.lang.Character.*;\nimport static javafx.scene.input.KeyCode.*;\nimport static javafx.scene.input.KeyCombination.*;\nimport static org.fxmisc.richtext.model.TwoDimensional.Bias.*;\nimport static org.fxmisc.wellbehaved.event.EventPattern.*;\nimport static org.fxmisc.wellbehaved.event.template.InputMapTemplate.*;\nimport static org.reactfx.EventStreams.*;\n\nimport java.util.function.Predicate;\n\nimport javafx.event.Event;\nimport javafx.geometry.Bounds;\nimport javafx.geometry.Point2D;\nimport javafx.scene.control.ContextMenu;\nimport javafx.scene.control.IndexRange;\nimport javafx.scene.input.ContextMenuEvent;\nimport javafx.scene.input.KeyCharacterCombination;\nimport javafx.scene.input.KeyEvent;\nimport javafx.scene.input.MouseButton;\nimport javafx.scene.input.MouseEvent;\n\nimport org.fxmisc.richtext.NavigationActions.SelectionPolicy;\nimport org.fxmisc.richtext.model.TwoDimensional.Position;\nimport org.fxmisc.wellbehaved.event.EventPattern;\nimport org.fxmisc.wellbehaved.event.InputHandler.Result;\nimport org.fxmisc.wellbehaved.event.template.InputMapTemplate;\nimport org.reactfx.EventStream;\nimport org.reactfx.value.Val;\nimport org.reactfx.value.Var;\n\n/**\n * Controller for GenericStyledArea.\n */\nclass GenericStyledAreaBehavior {\n\n    private static final boolean isMac;\n    private static final boolean isWindows;\n    static {\n        String os = System.getProperty("os.name");\n        isMac = os.startsWith("Mac");\n        isWindows = os.startsWith("Windows");\n    }\n\n    private static final InputMapTemplate<GenericStyledAreaBehavior, ? super Event> EVENT_TEMPLATE;\n    private static final Predicate<KeyEvent> controlKeysFilter;\n\n    static {\n        SelectionPolicy selPolicy = isMac\n                ? SelectionPolicy.EXTEND\n                : SelectionPolicy.ADJUST;\n\n        /*\n         * KeyCodes are misinterpreted when using a different keyboard layout, for example:\n         * on Dvorak: C results in KeyCode I, X -> B, and V -> . \n         * and on German layouts: Z and Y are reportedly switched\n         * so then editing commands such as Ctrl+C, or CMD+Z are incorrectly processed.\n         * KeyCharacterCombination however does keyboard translation before matching.\n         * This resolves issue #799\n         */\n        KeyCharacterCombination SHORTCUT_A = new KeyCharacterCombination( "a", SHORTCUT_DOWN );\n        KeyCharacterCombination SHORTCUT_C = new KeyCharacterCombination( "c", SHORTCUT_DOWN );\n        KeyCharacterCombination SHORTCUT_V = new KeyCharacterCombination( "v", SHORTCUT_DOWN );\n        KeyCharacterCombination SHORTCUT_X = new KeyCharacterCombination( "x", SHORTCUT_DOWN );\n        KeyCharacterCombination SHORTCUT_Y = new KeyCharacterCombination( "y", SHORTCUT_DOWN );\n        KeyCharacterCombination SHORTCUT_Z = new KeyCharacterCombination( "z", SHORTCUT_DOWN );\n        KeyCharacterCombination SHORTCUT_SHIFT_Z = new KeyCharacterCombination( "z", SHORTCUT_DOWN, SHIFT_DOWN );\n\n                // deletion\n                consume(keyPressed(DELETE),                     GenericStyledAreaBehavior::deleteForward),\n                consume(keyPressed(BACK_SPACE),                 GenericStyledAreaBehavior::deleteBackward),\n                consume(keyPressed(DELETE,     SHORTCUT_DOWN),  GenericStyledAreaBehavior::deleteNextWord),\n                consume(keyPressed(BACK_SPACE, SHORTCUT_DOWN),  GenericStyledAreaBehavior::deletePrevWord),\n                // cut\n                // cut\n                consume(\n                        anyOf(keyPressed(CUT),   keyPressed(SHORTCUT_X), keyPressed(DELETE, SHIFT_DOWN)),\n                        (b, e) -> b.view.cut()),\n                // paste\n                consume(\n                        anyOf(keyPressed(PASTE), keyPressed(SHORTCUT_V), keyPressed(INSERT, SHIFT_DOWN)),\n                        (b, e) -> b.view.paste()),\n                // tab & newline\n                consume(keyPressed(ENTER), (b, e) -> b.view.replaceSelection("\\n")),\n                consume(keyPressed(TAB),   (b, e) -> b.view.replaceSelection("\\t")),\n                // undo/redo\n                consume(keyPressed(SHORTCUT_Z), (b, e) -> b.view.undo()),\n                consume(\n                        anyOf(keyPressed(SHORTCUT_Y), keyPressed(SHORTCUT_SHIFT_Z)),\n                        (b, e) -> b.view.redo()),\n                // insert/overwrite\n                consume(keyPressed(INSERT), GenericStyledAreaBehavior::toggelOverwriteMode)\n        );\n        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> edits = when(b -> b.view.isEditable(), editsBase);\n\n        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> verticalNavigation = sequence(\n                // vertical caret movement\n                consume(\n                        anyOf(keyPressed(UP), keyPressed(KP_UP)),\n                        (b, e) -> b.prevLine(SelectionPolicy.CLEAR)),\n                consume(\n                        anyOf(keyPressed(DOWN), keyPressed(KP_DOWN)),\n                        (b, e) -> b.nextLine(SelectionPolicy.CLEAR)),\n                consume(keyPressed(PAGE_UP),    (b, e) -> b.view.prevPage(SelectionPolicy.CLEAR)),\n                consume(keyPressed(PAGE_DOWN),  (b, e) -> b.view.nextPage(SelectionPolicy.CLEAR)),\n                // vertical selection\n                consume(\n                        anyOf(keyPressed(UP,   SHIFT_DOWN), keyPressed(KP_UP, SHIFT_DOWN)),\n                        (b, e) -> b.prevLine(SelectionPolicy.ADJUST)),\n                consume(\n                        anyOf(keyPressed(DOWN, SHIFT_DOWN), keyPressed(KP_DOWN, SHIFT_DOWN)),\n                        (b, e) -> b.nextLine(SelectionPolicy.ADJUST)),\n                consume(keyPressed(PAGE_UP,   SHIFT_DOWN),  (b, e) -> b.view.prevPage(SelectionPolicy.ADJUST)),\n                consume(keyPressed(PAGE_DOWN, SHIFT_DOWN),  (b, e) -> b.view.nextPage(SelectionPolicy.ADJUST))\n        );\n\n        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> otherNavigation = sequence(\n                // caret movement\n                consume(anyOf(keyPressed(RIGHT), keyPressed(KP_RIGHT)), GenericStyledAreaBehavior::right),\n                consume(anyOf(keyPressed(LEFT),  keyPressed(KP_LEFT)),  GenericStyledAreaBehavior::left),\n                consume(keyPressed(HOME), (b, e) -> b.view.lineStart(SelectionPolicy.CLEAR)),\n                consume(keyPressed(END),  (b, e) -> b.view.lineEnd(SelectionPolicy.CLEAR)),\n                consume(\n                        anyOf(\n                                keyPressed(RIGHT,    SHORTCUT_DOWN),\n                                keyPressed(KP_RIGHT, SHORTCUT_DOWN)\n                        ), (b, e) -> b.skipToNextWord(SelectionPolicy.CLEAR)),\n                consume(\n                        anyOf(\n                                keyPressed(LEFT,     SHORTCUT_DOWN),\n                                keyPressed(KP_LEFT,  SHORTCUT_DOWN)\n                        ), (b, e) -> b.skipToPrevWord(SelectionPolicy.CLEAR)),\n                consume(keyPressed(HOME, SHORTCUT_DOWN), (b, e) -> b.view.start(SelectionPolicy.CLEAR)),\n                consume(keyPressed(END,  SHORTCUT_DOWN), (b, e) -> b.view.end(SelectionPolicy.CLEAR)),\n                // selection\n                consume(\n                        anyOf(\n                                keyPressed(RIGHT,    SHIFT_DOWN),\n                                keyPressed(KP_RIGHT, SHIFT_DOWN)\n                        ), GenericStyledAreaBehavior::selectRight),\n                consume(\n                        anyOf(\n                                keyPressed(LEFT,     SHIFT_DOWN),\n                                keyPressed(KP_LEFT,  SHIFT_DOWN)\n                        ), GenericStyledAreaBehavior::selectLeft),\n                consume(keyPressed(HOME, SHIFT_DOWN),                (b, e) -> b.view.lineStart(selPolicy)),\n                consume(keyPressed(END,  SHIFT_DOWN),                (b, e) -> b.view.lineEnd(selPolicy)),\n                consume(keyPressed(HOME, SHIFT_DOWN, SHORTCUT_DOWN), (b, e) -> b.view.start(selPolicy)),\n                consume(keyPressed(END,  SHIFT_DOWN, SHORTCUT_DOWN), (b, e) -> b.view.end(selPolicy)),\n                consume(\n                        anyOf(\n                                keyPressed(RIGHT,    SHIFT_DOWN, SHORTCUT_DOWN),\n                                keyPressed(KP_RIGHT, SHIFT_DOWN, SHORTCUT_DOWN)\n                        ), (b, e) -> b.skipToNextWord(selPolicy)),\n                consume(\n                        anyOf(\n                                keyPressed(LEFT,     SHIFT_DOWN, SHORTCUT_DOWN),\n                                keyPressed(KP_LEFT,  SHIFT_DOWN, SHORTCUT_DOWN)\n                        ), (b, e) -> b.skipToPrevWord(selPolicy)),\n                consume(keyPressed(SHORTCUT_A), (b, e) -> b.view.selectAll())\n        );\n\n        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> copyAction = consume(\n                anyOf(\n                        keyPressed(COPY),\n                        keyPressed(SHORTCUT_C),\n                        keyPressed(INSERT, SHORTCUT_DOWN)\n                ), (b, e) -> b.view.copy()\n        );\n\n        controlKeysFilter = e -> {\n            if (isWindows) {\n                //Windows input. ALT + CONTROL accelerators are the same as ALT GR accelerators.\n                //If ALT + CONTROL are pressed and the given character is valid then print the character.\n                //Else, don\'t consume the event. This change allows Windows users to use accelerators and\n                //printing special characters at the same time.\n                // (For example: ALT + CONTROL + E prints the euro symbol in the spanish keyboard while ALT + CONTROL + L has assigned an accelerator.)\n                //Note that this is how several IDEs such JetBrains IDEs or Eclipse behave.\n                if (e.isControlDown() && e.isAltDown() && !e.isMetaDown() && e.getCharacter().length() == 1\n                \t    && e.getCharacter().getBytes()[0] != 0) return true;\n                \n                return !e.isControlDown() && !e.isAltDown() && !e.isMetaDown();\n            }\n        \treturn !e.isControlDown() && !e.isMetaDown();\n        };\n\n        Predicate<KeyEvent> isChar = e ->\n                e.getCode().isLetterKey() ||\n                e.getCode().isDigitKey() ||\n                e.getCode().isWhitespaceKey();\n\n        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> charPressConsumer = consume(keyPressed().onlyIf(isChar.and(controlKeysFilter)));\n\n        InputMapTemplate<GenericStyledAreaBehavior, ? super KeyEvent> keyPressedTemplate = edits\n                .orElse(otherNavigation).ifConsumed((b, e) -> b.view.clearTargetCaretOffset())\n                .orElse(verticalNavigation)\n                .orElse(copyAction)\n                .ifConsumed((b, e) -> b.view.requestFollowCaret())\n                // no need to add \'ifConsumed\' after charPress since\n                // requestFollowCaret is called in keyTypedTemplate\n                .orElse(charPressConsumer);\n\n        InputMapTemplate<GenericStyledAreaBehavior, KeyEvent> keyTypedBase = consume(\n                // character input\n                EventPattern.keyTyped().onlyIf(controlKeysFilter.and(e -> isLegal(e.getCharacter()))),\n                GenericStyledAreaBehavior::keyTyped\n        ).ifConsumed((b, e) -> b.view.requestFollowCaret());\n        InputMapTemplate<GenericStyledAreaBehavior, ? super KeyEvent> keyTypedTemplate = when(b -> b.view.isEditable(), keyTypedBase);\n\n        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mousePressedTemplate = sequence(\n                // ignore mouse pressed events if the view is disabled\n                process(mousePressed(MouseButton.PRIMARY), (b, e) -> b.view.isDisabled() ? Result.IGNORE : Result.PROCEED),\n\n                // hide context menu before any other handling\n                process(\n                        mousePressed(), (b, e) -> {\n                            b.view.hideContextMenu();\n                            return Result.PROCEED;\n                        }\n                ),\n                consume(\n                        mousePressed(MouseButton.PRIMARY).onlyIf(MouseEvent::isShiftDown),\n                        GenericStyledAreaBehavior::handleShiftPress\n                ),\n                consume(\n                        mousePressed(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 1),\n                        GenericStyledAreaBehavior::handleFirstPrimaryPress\n                ),\n                consume(\n                        mousePressed(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 2),\n                        GenericStyledAreaBehavior::handleSecondPress\n                ),\n                consume(\n                        mousePressed(MouseButton.PRIMARY).onlyIf(e -> e.getClickCount() == 3),\n                        GenericStyledAreaBehavior::handleThirdPress\n                )\n        );\n\n        Predicate<MouseEvent> primaryOnlyButton = e -> e.getButton() == MouseButton.PRIMARY && !e.isMiddleButtonDown() && !e.isSecondaryButtonDown();\n\n        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mouseDragDetectedTemplate = consume(\n                eventType(MouseEvent.DRAG_DETECTED).onlyIf(primaryOnlyButton),\n                (b, e) -> b.handlePrimaryOnlyDragDetected()\n        );\n\n        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mouseDragTemplate = sequence(\n                process(\n                        mouseDragged().onlyIf(primaryOnlyButton),\n                        GenericStyledAreaBehavior::processPrimaryOnlyMouseDragged\n                ),\n                consume(\n                        mouseDragged(),\n                        GenericStyledAreaBehavior::continueOrStopAutoScroll\n                )\n        );\n\n        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mouseReleasedTemplate = sequence(\n                process(\n                        EventPattern.mouseReleased().onlyIf(primaryOnlyButton),\n                        GenericStyledAreaBehavior::processMouseReleased\n                ),\n                consume(\n                        mouseReleased(),\n                        (b, e) -> b.autoscrollTo.setValue(null)  // stop auto scroll\n                )\n        );\n\n        InputMapTemplate<GenericStyledAreaBehavior, ? super MouseEvent> mouseTemplate = sequence(\n                mousePressedTemplate, mouseDragDetectedTemplate, mouseDragTemplate, mouseReleasedTemplate\n        );\n\n        InputMapTemplate<GenericStyledAreaBehavior, ? super ContextMenuEvent> contextMenuEventTemplate = consumeWhen(\n                EventPattern.eventType(ContextMenuEvent.CONTEXT_MENU_REQUESTED),\n                b -> !b.view.isDisabled(),\n                GenericStyledAreaBehavior::showContextMenu\n        );\n\n        EVENT_TEMPLATE = sequence(mouseTemplate, keyPressedTemplate, keyTypedTemplate, contextMenuEventTemplate);\n    }\n\n    /**\n     * Possible dragging states.\n     */\n    private enum DragState {\n        /** No dragging is happening. */\n        NO_DRAG,\n\n        /** Mouse has been pressed inside of selected text, but drag has not been detected yet. */\n        POTENTIAL_DRAG,\n\n        /** Drag in progress. */\n        DRAG,\n    }\n\n    /* ********************************************************************** *\n     * Fields                                                                 *\n     * ********************************************************************** */\n\n    private final GenericStyledArea<?, ?, ?> view;\n\n    /**\n     * Indicates weather the area is in overwrite or insert mode.\n     */\n    private boolean overwriteMode = false;\n\n    /**\n     * Indicates whether an existing selection is being dragged by the user.\n     */\n    private DragState dragSelection = DragState.NO_DRAG;\n\n    /**\n     * Indicates whether a new selection is being made by the user.\n     */\n    private DragState dragNewSelection = DragState.NO_DRAG;\n\n    private final Var<Point2D> autoscrollTo = Var.newSimpleVar(null);\n\n    /* ********************************************************************** *\n     * Constructors                                                           *\n     * ********************************************************************** */\n\n    GenericStyledAreaBehavior(GenericStyledArea<?, ?, ?> area) {\n        this.view = area;\n\n        InputMapTemplate.installFallback(EVENT_TEMPLATE, this, b -> b.view);\n\n        // setup auto-scroll\n        Val<Point2D> projection = Val.combine(\n                autoscrollTo,\n                area.layoutBoundsProperty(),\n                GenericStyledAreaBehavior::project);\n        Val<Point2D> distance = Val.combine(\n                autoscrollTo,\n                projection,\n                Point2D::subtract);\n        EventStream<Point2D> deltas = nonNullValuesOf(distance)\n                .emitBothOnEach(animationFrames())\n                .map(t -> t.map((ds, nanos) -> ds.multiply(nanos / 100_000_000.0)));\n        valuesOf(autoscrollTo).flatMap(p -> p == null\n                ? never() // automatically stops the scroll animation\n                : deltas)\n            .subscribe(ds -> {\n                area.scrollBy(ds);\n                projection.ifPresent(this::dragTo);\n            });\n    }\n\n    /* ********************************************************************** *\n     * Key handling implementation                                            *\n     * ********************************************************************** */\n\n    private void keyTyped(KeyEvent event) {\n        String text = event.getCharacter();\n        int n = text.length();\n\n        if(n == 0) {\n            return;\n        }\n\n        IndexRange range = view.getSelection();\n        int start = range.getStart();\n        int end = range.getEnd();\n\n        if (overwriteMode && start == end) {\n            end = Math.min(end+1, view.getLength());\n        }\n\n        view.replaceText(start, end, text);\n    }\n\n    private void toggelOverwriteMode(KeyEvent ignore) {\n        overwriteMode = !overwriteMode;\n    }\n\n    private void deleteBackward(KeyEvent ignore) {\n        IndexRange selection = view.getSelection();\n        if(selection.getLength() == 0) {\n            view.deletePreviousChar();\n        } else {\n            view.replaceSelection("");\n        }\n    }\n\n    private void deleteForward(KeyEvent ignore) {\n        IndexRange selection = view.getSelection();\n        if(selection.getLength() == 0) {\n            view.deleteNextChar();\n        } else {\n            view.replaceSelection("");\n        }\n    }\n\n    private void left(KeyEvent ignore) {\n        IndexRange sel = view.getSelection();\n        if(sel.getLength() == 0) {\n            view.previousChar(SelectionPolicy.CLEAR);\n        } else {\n            view.moveTo(sel.getStart(), SelectionPolicy.CLEAR);\n        }\n    }\n\n    private void right(KeyEvent ignore) {\n        IndexRange sel = view.getSelection();\n        if(sel.getLength() == 0) {\n            view.nextChar(SelectionPolicy.CLEAR);\n        } else {\n            view.moveTo(sel.getEnd(), SelectionPolicy.CLEAR);\n        }\n    }\n\n    private void selectLeft(KeyEvent ignore) {\n        view.previousChar(SelectionPolicy.ADJUST);\n    }\n\n    private void selectRight(KeyEvent ignore) {\n        view.nextChar(SelectionPolicy.ADJUST);\n    }\n\n    private void deletePrevWord(KeyEvent ignore) {\n        int end = view.getCaretPosition();\n\n        if (end > 0) {\n            view.wordBreaksBackwards(2, SelectionPolicy.CLEAR);\n            int start = view.getCaretPosition();\n            view.replaceText(start, end, "");\n        }\n    }\n\n    private void deleteNextWord(KeyEvent ignore) {\n        int start = view.getCaretPosition();\n\n        if (start < view.getLength()) {\n            view.wordBreaksForwards(2, SelectionPolicy.CLEAR);\n            int end = view.getCaretPosition();\n            view.replaceText(start, end, "");\n        }\n    }\n\n    private void downLines(SelectionPolicy selectionPolicy, int nLines) {\n        Position currentLine = view.currentLine();\n        Position targetLine = currentLine.offsetBy(nLines, Forward).clamp();\n        if(!currentLine.sameAs(targetLine)) {\n            // compute new caret position\n            CharacterHit hit = view.hit(view.getTargetCaretOffset(), targetLine);\n\n            // update model\n            view.moveTo(hit.getInsertionIndex(), selectionPolicy);\n        }\n    }\n\n    private void prevLine(SelectionPolicy selectionPolicy) {\n        downLines(selectionPolicy, -1);\n    }\n\n    private void nextLine(SelectionPolicy selectionPolicy) {\n        downLines(selectionPolicy, 1);\n    }\n\n    private void skipToPrevWord(SelectionPolicy selectionPolicy) {\n        int caretPos = view.getCaretPosition();\n\n        // if (0 == caretPos), do nothing as can\'t move to the left anyway\n        if (1 <= caretPos ) {\n            boolean prevCharIsWhiteSpace = isWhitespace(view.getText(caretPos - 1, caretPos).charAt(0));\n            view.wordBreaksBackwards(prevCharIsWhiteSpace ? 2 : 1, selectionPolicy);\n        }\n    }\n\n    private void skipToNextWord(SelectionPolicy selectionPolicy) {\n        int caretPos = view.getCaretPosition();\n        int length = view.getLength();\n\n        // if (caretPos == length), do nothing as can\'t move to the right anyway\n        if (caretPos <= length - 1) {\n            boolean nextCharIsWhiteSpace = isWhitespace(view.getText(caretPos, caretPos + 1).charAt(0));\n            view.wordBreaksForwards(nextCharIsWhiteSpace ? 2 : 1, selectionPolicy);\n        }\n    }\n\n    /* ********************************************************************** *\n     * Mouse handling implementation                                          *\n     * ********************************************************************** */\n\n    private void showContextMenu(ContextMenuEvent e) {\n        view.requestFocus();\n        if ( view.isContextMenuPresent() ) {\n            ContextMenu menu = view.getContextMenu();\n            double x = e.getScreenX() + view.getContextMenuXOffset();\n            double y = e.getScreenY() + view.getContextMenuYOffset();\n            menu.show( view, x, y );\n        }\n    }\n\n    private void handleShiftPress(MouseEvent e) {\n        // ensure focus\n        view.requestFocus();\n\n        CharacterHit hit = view.hit(e.getX(), e.getY());\n\n        // On Mac always extend selection,\n        // switching anchor and caret if necessary.\n        view.moveTo(hit.getInsertionIndex(), isMac ? SelectionPolicy.EXTEND : SelectionPolicy.ADJUST);\n    }\n\n    private void handleFirstPrimaryPress(MouseEvent e) {\n        // ensure focus\n        view.requestFocus();\n\n        CharacterHit hit = view.hit(e.getX(), e.getY());\n\n        view.clearTargetCaretOffset();\n        IndexRange selection = view.getSelection();\n        if(view.isEditable() &&\n                selection.getLength() != 0 &&\n                hit.getCharacterIndex().isPresent() &&\n                hit.getCharacterIndex().getAsInt() >= selection.getStart() &&\n                hit.getCharacterIndex().getAsInt() < selection.getEnd()) {\n            // press inside selection\n            dragSelection = DragState.POTENTIAL_DRAG;\n            dragNewSelection = DragState.NO_DRAG;\n        } else {\n            dragSelection = DragState.NO_DRAG;\n            dragNewSelection = DragState.NO_DRAG;\n            view.getOnOutsideSelectionMousePressed().handle(e);\n        }\n    }\n\n    private void handleSecondPress(MouseEvent e) {\n        view.selectWord();\n    }\n\n    private void handleThirdPress(MouseEvent e) {\n        view.selectParagraph();\n    }\n\n    private void handlePrimaryOnlyDragDetected() {\n        if (dragSelection == DragState.POTENTIAL_DRAG) {\n            dragSelection = DragState.DRAG;\n        }\n        else dragNewSelection = DragState.DRAG;\n    }\n\n    private Result processPrimaryOnlyMouseDragged(MouseEvent e) {\n        Point2D p = new Point2D(e.getX(), e.getY());\n        if(view.getLayoutBounds().contains(p)) {\n            dragTo(p);\n        }\n        view.setAutoScrollOnDragDesired(true);\n        // autoScrollTo will be set in "continueOrStopAutoScroll(MouseEvent)"\n        return Result.PROCEED;\n    }\n\n    private void continueOrStopAutoScroll(MouseEvent e) {\n        if (!view.isAutoScrollOnDragDesired()) {\n            autoscrollTo.setValue(null); // stops auto-scroll\n        }\n\n        Point2D p = new Point2D(e.getX(), e.getY());\n        if(view.getLayoutBounds().contains(p)) {\n            autoscrollTo.setValue(null); // stops auto-scroll\n        } else {\n            autoscrollTo.setValue(p);    // starts auto-scroll\n        }\n    }\n\n    private void dragTo(Point2D point) {\n        if(dragSelection == DragState.DRAG ||\n                dragSelection == DragState.POTENTIAL_DRAG) { // MOUSE_DRAGGED may arrive even before DRAG_DETECTED\n            view.getOnSelectionDrag().accept(point);\n        } else {\n            view.getOnNewSelectionDrag().accept(point);\n        }\n    }\n\n    private Result processMouseReleased(MouseEvent e) {\n        if (view.isDisabled()) {\n            return Result.IGNORE;\n        }\n\n        switch(dragSelection) {\n            case POTENTIAL_DRAG:\n                // selection was not dragged, but clicked\n                view.getOnInsideSelectionMousePressReleased().handle(e);\n            case DRAG:\n                view.getOnSelectionDropped().handle(e);\n                break;\n            case NO_DRAG: if ( dragNewSelection == DragState.DRAG ) {\n                view.getOnNewSelectionDragFinished().handle(e);\n            }\n        }\n        dragNewSelection = DragState.NO_DRAG;\n        dragSelection = DragState.NO_DRAG;\n\n        return Result.PROCEED;\n    }\n\n    private static Point2D project(Point2D p, Bounds bounds) {\n        double x = clamp(p.getX(), bounds.getMinX(), bounds.getMaxX());\n        double y = clamp(p.getY(), bounds.getMinY(), bounds.getMaxY());\n        return new Point2D(x, y);\n    }\n\n    private static double clamp(double x, double min, double max) {\n        return Math.min(Math.max(x, min), max);\n    }\n\n    static boolean isControlKeyEvent(KeyEvent event) {\n    \treturn controlKeysFilter.test(event);\n    }\n\n    private static boolean isLegal(String text) {\n        int n = text.length();\n        for(int i = 0; i < n; ++i) {\n            if(Character.isISOControl(text.charAt(i))) {\n                return false;\n            }\n        }\n        return true;\n    }\n}\n', 'message': 'Added shift backspace to delete backward', 'author_name': 'Jurgen', 'lang': 'java'}