1 /*
   2  * Copyright (c) 2011, 2015, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package com.sun.javafx.scene.control.behavior;
  27 
  28 import com.sun.javafx.PlatformUtil;
  29 import com.sun.javafx.geom.transform.Affine3D;
  30 import com.sun.javafx.scene.control.Properties;
  31 import javafx.scene.control.skin.TextAreaSkin;
  32 import javafx.beans.value.ChangeListener;
  33 import javafx.beans.value.ObservableValue;
  34 import javafx.geometry.Bounds;
  35 import javafx.geometry.Point2D;
  36 import javafx.geometry.Rectangle2D;
  37 import javafx.scene.Scene;
  38 import javafx.scene.control.ContextMenu;
  39 import javafx.scene.control.TextArea;
  40 import com.sun.javafx.scene.control.skin.Utils;
  41 import javafx.scene.input.ContextMenuEvent;
  42 import com.sun.javafx.scene.control.inputmap.InputMap;
  43 import com.sun.javafx.scene.control.inputmap.KeyBinding;
  44 import javafx.scene.input.KeyEvent;
  45 import javafx.scene.input.MouseButton;
  46 import javafx.scene.input.MouseEvent;
  47 import javafx.stage.Screen;
  48 import javafx.stage.Window;
  49 
  50 import java.util.function.Predicate;
  51 
  52 import static javafx.scene.control.skin.TextAreaSkin.TextPosInfo;
  53 
  54 import static com.sun.javafx.PlatformUtil.isMac;
  55 import static com.sun.javafx.PlatformUtil.isWindows;
  56 import static javafx.scene.control.skin.TextInputControlSkin.TextUnit;
  57 import static javafx.scene.control.skin.TextInputControlSkin.Direction;
  58 import static javafx.scene.input.KeyCode.*;
  59 
  60 
  61 /**
  62  * Text area behavior.
  63  */
  64 public class TextAreaBehavior extends TextInputControlBehavior<TextArea> {
  65 //    /**************************************************************************
  66 //     *                          Setup KeyBindings                             *
  67 //     *************************************************************************/
  68 //    protected static final List<KeyBinding> TEXT_AREA_BINDINGS = new ArrayList<KeyBinding>();
  69 //    static {
  70 //        TEXT_AREA_BINDINGS.add(new KeyBinding(HOME, KEY_PRESSED, "LineStart")); // changed
  71 //        TEXT_AREA_BINDINGS.add(new KeyBinding(END, KEY_PRESSED, "LineEnd")); // changed
  72 //        TEXT_AREA_BINDINGS.add(new KeyBinding(UP, KEY_PRESSED, "PreviousLine")); // changed
  73 //        TEXT_AREA_BINDINGS.add(new KeyBinding(KP_UP, KEY_PRESSED, "PreviousLine")); // changed
  74 //        TEXT_AREA_BINDINGS.add(new KeyBinding(DOWN, KEY_PRESSED, "NextLine")); // changed
  75 //        TEXT_AREA_BINDINGS.add(new KeyBinding(KP_DOWN, KEY_PRESSED, "NextLine")); // changed
  76 //        TEXT_AREA_BINDINGS.add(new KeyBinding(PAGE_UP, KEY_PRESSED, "PreviousPage")); // new
  77 //        TEXT_AREA_BINDINGS.add(new KeyBinding(PAGE_DOWN, KEY_PRESSED, "NextPage")); // new
  78 //        TEXT_AREA_BINDINGS.add(new KeyBinding(ENTER, KEY_PRESSED, "InsertNewLine")); // changed
  79 //        TEXT_AREA_BINDINGS.add(new KeyBinding(TAB, KEY_PRESSED, "TraverseOrInsertTab")); // changed
  80 //
  81 //        TEXT_AREA_BINDINGS.add(new KeyBinding(HOME, KEY_PRESSED, "SelectLineStart").shift()); // changed
  82 //        TEXT_AREA_BINDINGS.add(new KeyBinding(END, KEY_PRESSED, "SelectLineEnd").shift()); // changed
  83 //        TEXT_AREA_BINDINGS.add(new KeyBinding(UP, KEY_PRESSED, "SelectPreviousLine").shift()); // changed
  84 //        TEXT_AREA_BINDINGS.add(new KeyBinding(KP_UP, KEY_PRESSED, "SelectPreviousLine").shift()); // changed
  85 //        TEXT_AREA_BINDINGS.add(new KeyBinding(DOWN, KEY_PRESSED, "SelectNextLine").shift()); // changed
  86 //        TEXT_AREA_BINDINGS.add(new KeyBinding(KP_DOWN, KEY_PRESSED, "SelectNextLine").shift()); // changed
  87 //        TEXT_AREA_BINDINGS.add(new KeyBinding(PAGE_UP, KEY_PRESSED, "SelectPreviousPage").shift()); // new
  88 //        TEXT_AREA_BINDINGS.add(new KeyBinding(PAGE_DOWN, KEY_PRESSED, "SelectNextPage").shift()); // new
  89 //        // Platform specific settings
  90 //        if (isMac()) {
  91 //            TEXT_AREA_BINDINGS.add(new KeyBinding(LEFT, KEY_PRESSED, "LineStart").shortcut()); // changed
  92 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_LEFT, KEY_PRESSED, "LineStart").shortcut()); // changed
  93 //            TEXT_AREA_BINDINGS.add(new KeyBinding(RIGHT, KEY_PRESSED, "LineEnd").shortcut()); // changed
  94 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_RIGHT, KEY_PRESSED, "LineEnd").shortcut()); // changed
  95 //            TEXT_AREA_BINDINGS.add(new KeyBinding(UP, KEY_PRESSED, "Home").shortcut());
  96 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_UP, KEY_PRESSED, "Home").shortcut());
  97 //            TEXT_AREA_BINDINGS.add(new KeyBinding(DOWN, KEY_PRESSED, "End").shortcut());
  98 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_DOWN, KEY_PRESSED, "End").shortcut());
  99 //
 100 //            TEXT_AREA_BINDINGS.add(new KeyBinding(LEFT, KEY_PRESSED, "SelectLineStartExtend").shift().shortcut()); // changed
 101 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_LEFT, KEY_PRESSED, "SelectLineStartExtend").shift().shortcut()); // changed
 102 //            TEXT_AREA_BINDINGS.add(new KeyBinding(RIGHT, KEY_PRESSED, "SelectLineEndExtend").shift().shortcut()); // changed
 103 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_RIGHT, KEY_PRESSED, "SelectLineEndExtend").shift().shortcut()); // changed
 104 //            TEXT_AREA_BINDINGS.add(new KeyBinding(UP, KEY_PRESSED, "SelectHomeExtend").shortcut().shift());
 105 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_UP, KEY_PRESSED, "SelectHomeExtend").shortcut().shift());
 106 //            TEXT_AREA_BINDINGS.add(new KeyBinding(DOWN, KEY_PRESSED, "SelectEndExtend").shortcut().shift());
 107 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_DOWN, KEY_PRESSED, "SelectEndExtend").shortcut().shift());
 108 //
 109 //            TEXT_AREA_BINDINGS.add(new KeyBinding(UP, KEY_PRESSED, "ParagraphStart").alt());
 110 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_UP, KEY_PRESSED, "ParagraphStart").alt());
 111 //            TEXT_AREA_BINDINGS.add(new KeyBinding(DOWN, KEY_PRESSED, "ParagraphEnd").alt());
 112 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_DOWN, KEY_PRESSED, "ParagraphEnd").alt());
 113 //
 114 //            TEXT_AREA_BINDINGS.add(new KeyBinding(UP, KEY_PRESSED, "SelectParagraphStart").alt().shift());
 115 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_UP, KEY_PRESSED, "SelectParagraphStart").alt().shift());
 116 //            TEXT_AREA_BINDINGS.add(new KeyBinding(DOWN, KEY_PRESSED, "SelectParagraphEnd").alt().shift());
 117 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_DOWN, KEY_PRESSED, "SelectParagraphEnd").alt().shift());
 118 //        } else {
 119 //            TEXT_AREA_BINDINGS.add(new KeyBinding(UP, KEY_PRESSED, "ParagraphStart").ctrl());
 120 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_UP, KEY_PRESSED, "ParagraphStart").ctrl());
 121 //            TEXT_AREA_BINDINGS.add(new KeyBinding(DOWN, KEY_PRESSED, "ParagraphEnd").ctrl());
 122 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_DOWN, KEY_PRESSED, "ParagraphEnd").ctrl());
 123 //            TEXT_AREA_BINDINGS.add(new KeyBinding(UP, KEY_PRESSED, "SelectParagraphStart").ctrl().shift());
 124 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_UP, KEY_PRESSED, "SelectParagraphStart").ctrl().shift());
 125 //            TEXT_AREA_BINDINGS.add(new KeyBinding(DOWN, KEY_PRESSED, "SelectParagraphEnd").ctrl().shift());
 126 //            TEXT_AREA_BINDINGS.add(new KeyBinding(KP_DOWN, KEY_PRESSED, "SelectParagraphEnd").ctrl().shift());
 127 //        }
 128 //        // Add the other standard key bindings in
 129 //        TEXT_AREA_BINDINGS.addAll(TextInputControlBindings.BINDINGS);
 130 //        // However, we want to consume other key press / release events too, for
 131 //        // things that would have been handled by the InputCharacter normally
 132 //        TEXT_AREA_BINDINGS.add(new KeyBinding(null, KEY_PRESSED, "Consume"));
 133 //    }
 134 //
 135     private TextAreaSkin skin;
 136     private TwoLevelFocusBehavior tlFocus;
 137 
 138     /**************************************************************************
 139      * Constructors                                                           *
 140      *************************************************************************/
 141 
 142     public TextAreaBehavior(final TextArea c) {
 143         super(c);
 144 
 145         if (Properties.IS_TOUCH_SUPPORTED) {
 146             contextMenu.getStyleClass().add("text-input-context-menu");
 147         }
 148 
 149         // some of the mappings are only valid when the control is editable, or
 150         // only on certain platforms, so we create the following predicates that filters out the mapping when the
 151         // control is not in the correct state / on the correct platform
 152         final Predicate<KeyEvent> validWhenEditable = e -> !c.isEditable();
 153         final Predicate<KeyEvent> validOnMac = e -> !PlatformUtil.isMac();
 154         final Predicate<KeyEvent> validOnWindows = e -> !PlatformUtil.isWindows();
 155         final Predicate<KeyEvent> validOnLinux = e -> !PlatformUtil.isLinux();
 156 
 157         // Add these bindings as a child input map, so they take precedence
 158         InputMap<TextArea> textAreaInputMap = new InputMap<>(c);
 159         textAreaInputMap.getMappings().addAll(
 160             keyMapping(HOME,      e -> lineStart(false)),
 161             keyMapping(END,       e -> lineEnd(false)),
 162             keyMapping(UP,        e -> skin.moveCaret(TextUnit.LINE, Direction.UP,   false)),
 163             keyMapping(DOWN,      e -> skin.moveCaret(TextUnit.LINE, Direction.DOWN, false)),
 164             keyMapping(PAGE_UP,   e -> skin.moveCaret(TextUnit.PAGE, Direction.UP,   false)),
 165             keyMapping(PAGE_DOWN, e -> skin.moveCaret(TextUnit.PAGE, Direction.DOWN, false)),
 166 
 167             keyMapping(new KeyBinding(HOME).shift(),      e -> lineStart(true)),
 168             keyMapping(new KeyBinding(END).shift(),       e -> lineEnd(true)),
 169             keyMapping(new KeyBinding(UP).shift(),        e -> skin.moveCaret(TextUnit.LINE, Direction.UP,   true)),
 170             keyMapping(new KeyBinding(DOWN).shift(),      e -> skin.moveCaret(TextUnit.LINE, Direction.DOWN, true)),
 171             keyMapping(new KeyBinding(PAGE_UP).shift(),   e -> skin.moveCaret(TextUnit.PAGE, Direction.UP,   true)),
 172             keyMapping(new KeyBinding(PAGE_DOWN).shift(), e -> skin.moveCaret(TextUnit.PAGE, Direction.DOWN, true)),
 173 
 174             // editing-only mappings
 175             keyMapping(new KeyBinding(ENTER), e -> insertNewLine(), validWhenEditable),
 176             keyMapping(new KeyBinding(TAB), e -> insertTab(), validWhenEditable)
 177         );
 178         addDefaultChildMap(getInputMap(), textAreaInputMap);
 179 
 180         // mac os specific mappings
 181         InputMap<TextArea> macOsInputMap = new InputMap<>(c);
 182         macOsInputMap.setInterceptor(e -> !PlatformUtil.isMac());
 183         macOsInputMap.getMappings().addAll(
 184             // Mac OS specific mappings
 185             keyMapping(new KeyBinding(LEFT).shortcut(),  e -> lineStart(false)),
 186             keyMapping(new KeyBinding(RIGHT).shortcut(), e -> lineEnd(false)),
 187             keyMapping(new KeyBinding(UP).shortcut(),    e -> c.home()),
 188             keyMapping(new KeyBinding(DOWN).shortcut(),  e -> c.end()),
 189 
 190             keyMapping(new KeyBinding(LEFT).shortcut().shift(),  e -> lineStart(true)),
 191             keyMapping(new KeyBinding(RIGHT).shortcut().shift(), e -> lineEnd(true)),
 192             keyMapping(new KeyBinding(UP).shortcut().shift(),    e -> selectHomeExtend()),
 193             keyMapping(new KeyBinding(DOWN).shortcut().shift(),  e -> selectEndExtend()),
 194 
 195             keyMapping(new KeyBinding(UP).alt(),           e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.UP,   false)),
 196             keyMapping(new KeyBinding(DOWN).alt(),         e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.DOWN, false)),
 197             keyMapping(new KeyBinding(UP).alt().shift(),   e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.UP,   true)),
 198             keyMapping(new KeyBinding(DOWN).alt().shift(), e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.DOWN, true))
 199         );
 200         addDefaultChildMap(textAreaInputMap, macOsInputMap);
 201 
 202         // windows / linux specific mappings
 203         InputMap<TextArea> nonMacOsInputMap = new InputMap<>(c);
 204         nonMacOsInputMap.setInterceptor(e -> PlatformUtil.isMac());
 205         nonMacOsInputMap.getMappings().addAll(
 206             keyMapping(new KeyBinding(UP).ctrl(),           e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.UP,   false)),
 207             keyMapping(new KeyBinding(DOWN).ctrl(),         e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.DOWN, false)),
 208             keyMapping(new KeyBinding(UP).ctrl().shift(),   e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.UP,   true)),
 209             keyMapping(new KeyBinding(DOWN).ctrl().shift(), e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.DOWN, true))
 210         );
 211         addDefaultChildMap(textAreaInputMap, nonMacOsInputMap);
 212 
 213         addKeyPadMappings(textAreaInputMap);
 214 
 215         // Register for change events
 216         c.focusedProperty().addListener(new ChangeListener<Boolean>() {
 217             @Override
 218             public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
 219                 // NOTE: The code in this method is *almost* and exact copy of what is in TextFieldBehavior.
 220                 // The only real difference is that TextFieldBehavior selects all the text when the control
 221                 // receives focus (when not gained by mouse click), whereas TextArea doesn't, and also the
 222                 // TextArea doesn't lose selection on focus lost, whereas the TextField does.
 223                 final TextArea textArea = getNode();
 224                 if (textArea.isFocused()) {
 225                     if (PlatformUtil.isIOS()) {
 226                         // Special handling of focus on iOS is required to allow to
 227                         // control native keyboard, because native keyboard is popped-up only when native
 228                         // text component gets focus. When we have JFX keyboard we can remove this code
 229                         final Bounds bounds = textArea.getBoundsInParent();
 230                         double w = bounds.getWidth();
 231                         double h = bounds.getHeight();
 232                         Affine3D trans = TextFieldBehavior.calculateNodeToSceneTransform(textArea);
 233                         String text = textArea.textProperty().getValueSafe();
 234 
 235                         // we need to display native text input component on the place where JFX component is drawn
 236                         // all parameters needed to do that are passed to native impl. here
 237                         textArea.getScene().getWindow().impl_getPeer().requestInput(text, TextFieldBehavior.TextInputTypes.TEXT_AREA.ordinal(), w, h,
 238                                 trans.getMxx(), trans.getMxy(), trans.getMxz(), trans.getMxt(),
 239                                 trans.getMyx(), trans.getMyy(), trans.getMyz(), trans.getMyt(),
 240                                 trans.getMzx(), trans.getMzy(), trans.getMzz(), trans.getMzt());
 241                     }
 242                     if (!focusGainedByMouseClick) {
 243                         setCaretAnimating(true);
 244                     }
 245                 } else {
 246 //                    skin.hideCaret();
 247                     if (PlatformUtil.isIOS() && textArea.getScene() != null) {
 248                         // releasing the focus => we need to hide the native component and also native keyboard
 249                         textArea.getScene().getWindow().impl_getPeer().releaseInput();
 250                     }
 251                     focusGainedByMouseClick = false;
 252                     setCaretAnimating(false);
 253                 }
 254             }
 255         });
 256 
 257         // Only add this if we're on an embedded platform that supports 5-button navigation
 258         if (Utils.isTwoLevelFocus()) {
 259             tlFocus = new TwoLevelFocusBehavior(c); // needs to be last.
 260         }
 261     }
 262 
 263     @Override public void dispose() {
 264         if (tlFocus != null) tlFocus.dispose();
 265         super.dispose();
 266     }
 267 
 268     // An unholy back-reference!
 269     public void setTextAreaSkin(TextAreaSkin skin) {
 270         this.skin = skin;
 271     }
 272 
 273     private void insertNewLine() {
 274         setEditing(true);
 275         getNode().replaceSelection("\n");
 276         setEditing(false);
 277     }
 278 
 279     private void insertTab() {
 280         setEditing(true);
 281         getNode().replaceSelection("\t");
 282         setEditing(false);
 283     }
 284 
 285     @Override protected void deleteChar(boolean previous) {
 286         if (previous) {
 287             getNode().deletePreviousChar();
 288         } else {
 289             getNode().deleteNextChar();
 290         }
 291     }
 292 
 293     @Override protected void deleteFromLineStart() {
 294         TextArea textArea = getNode();
 295         int end = textArea.getCaretPosition();
 296 
 297         if (end > 0) {
 298             lineStart(false);
 299             int start = textArea.getCaretPosition();
 300             if (end > start) {
 301                 replaceText(start, end, "");
 302             }
 303         }
 304     }
 305 
 306     private void lineStart(boolean select) {
 307         skin.moveCaret(TextUnit.LINE, Direction.BEGINNING, select);
 308     }
 309 
 310     private void lineEnd(boolean select) {
 311         skin.moveCaret(TextUnit.LINE, Direction.END, select);
 312     }
 313 
 314     @Override protected void replaceText(int start, int end, String txt) {
 315         getNode().replaceText(start, end, txt);
 316     }
 317 
 318     /**
 319      * If the focus is gained via response to a mouse click, then we don't
 320      * want to select all the text even if selectOnFocus is true.
 321      */
 322     private boolean focusGainedByMouseClick = false; // TODO!!
 323     private boolean shiftDown = false;
 324     private boolean deferClick = false;
 325 
 326     @Override public void mousePressed(MouseEvent e) {
 327         TextArea textArea = getNode();
 328         // We never respond to events if disabled
 329         if (!textArea.isDisabled()) {
 330             // If the text field doesn't have focus, then we'll attempt to set
 331             // the focus and we'll indicate that we gained focus by a mouse
 332             // click, TODO which will then NOT honor the selectOnFocus variable
 333             // of the textInputControl
 334             if (!textArea.isFocused()) {
 335                 focusGainedByMouseClick = true;
 336                 textArea.requestFocus();
 337             }
 338 
 339             // stop the caret animation
 340             setCaretAnimating(false);
 341             // only if there is no selection should we see the caret
 342 //            setCaretOpacity(if (textInputControl.dot == textInputControl.mark) then 1.0 else 0.0);
 343 
 344             // if the primary button was pressed
 345             if (e.getButton() == MouseButton.PRIMARY && !(e.isMiddleButtonDown() || e.isSecondaryButtonDown())) {
 346                 TextPosInfo hit = skin.getIndex(e.getX(), e.getY());
 347                 int i = Utils.getHitInsertionIndex(hit, textArea.textProperty().getValueSafe());
 348 //                 int i = skin.getInsertionPoint(e.getX(), e.getY());
 349                 final int anchor = textArea.getAnchor();
 350                 final int caretPosition = textArea.getCaretPosition();
 351                 if (e.getClickCount() < 2 &&
 352                     (e.isSynthesized() ||
 353                      (anchor != caretPosition &&
 354                       ((i > anchor && i < caretPosition) || (i < anchor && i > caretPosition))))) {
 355                     // if there is a selection, then we will NOT handle the
 356                     // press now, but will defer until the release. If you
 357                     // select some text and then press down, we change the
 358                     // caret and wait to allow you to drag the text (TODO).
 359                     // When the drag concludes, then we handle the click
 360 
 361                     deferClick = true;
 362                     // TODO start a timer such that after some millis we
 363                     // switch into text dragging mode, change the cursor
 364                     // to indicate the text can be dragged, etc.
 365                 } else if (!(e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown() || e.isShortcutDown())) {
 366                     switch (e.getClickCount()) {
 367                         case 1: skin.positionCaret(hit, false); break;
 368                         case 2: mouseDoubleClick(hit); break;
 369                         case 3: mouseTripleClick(hit); break;
 370                         default: // no-op
 371                     }
 372                 } else if (e.isShiftDown() && !(e.isControlDown() || e.isAltDown() || e.isMetaDown() || e.isShortcutDown()) && e.getClickCount() == 1) {
 373                     // didn't click inside the selection, so select
 374                     shiftDown = true;
 375                     // if we are on mac os, then we will accumulate the
 376                     // selection instead of just moving the dot. This happens
 377                     // by figuring out past which (dot/mark) are extending the
 378                     // selection, and set the mark to be the other side and
 379                     // the dot to be the new position.
 380                     // everywhere else we just move the dot.
 381                     if (isMac()) {
 382                         textArea.extendSelection(i);
 383                     } else {
 384                         skin.positionCaret(hit, true);
 385                     }
 386                 }
 387 //                 skin.setForwardBias(hit.isLeading());
 388 //                if (textInputControl.editable)
 389 //                    displaySoftwareKeyboard(true);
 390             }
 391             if (contextMenu.isShowing()) {
 392                 contextMenu.hide();
 393             }
 394         }
 395     }
 396 
 397     @Override public void mouseDragged(MouseEvent e) {
 398         final TextArea textArea = getNode();
 399         // we never respond to events if disabled, but we do notify any onXXX
 400         // event listeners on the control
 401         if (!textArea.isDisabled() && !e.isSynthesized()) {
 402             if (e.getButton() == MouseButton.PRIMARY &&
 403                     !(e.isMiddleButtonDown() || e.isSecondaryButtonDown() ||
 404                             e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown())) {
 405                 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), true);
 406             }
 407         }
 408         deferClick = false;
 409     }
 410 
 411     @Override public void mouseReleased(final MouseEvent e) {
 412         final TextArea textArea = getNode();
 413         // we never respond to events if disabled, but we do notify any onXXX
 414         // event listeners on the control
 415         if (!textArea.isDisabled()) {
 416             setCaretAnimating(false);
 417             if (deferClick) {
 418                 deferClick = false;
 419                 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), shiftDown);
 420                 shiftDown = false;
 421             }
 422             setCaretAnimating(true);
 423         }
 424     }
 425 
 426     @Override public void contextMenuRequested(ContextMenuEvent e) {
 427         final TextArea textArea = getNode();
 428 
 429         if (contextMenu.isShowing()) {
 430             contextMenu.hide();
 431         } else if (textArea.getContextMenu() == null &&
 432                    textArea.getOnContextMenuRequested() == null) {
 433             double screenX = e.getScreenX();
 434             double screenY = e.getScreenY();
 435             double sceneX = e.getSceneX();
 436 
 437             if (Properties.IS_TOUCH_SUPPORTED) {
 438                 Point2D menuPos;
 439                 if (textArea.getSelection().getLength() == 0) {
 440                     skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false);
 441                     menuPos = skin.getMenuPosition();
 442                 } else {
 443                     menuPos = skin.getMenuPosition();
 444                     if (menuPos != null && (menuPos.getX() <= 0 || menuPos.getY() <= 0)) {
 445                         skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false);
 446                         menuPos = skin.getMenuPosition();
 447                     }
 448                 }
 449 
 450                 if (menuPos != null) {
 451                     Point2D p = getNode().localToScene(menuPos);
 452                     Scene scene = getNode().getScene();
 453                     Window window = scene.getWindow();
 454                     Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(),
 455                                                    window.getY() + scene.getY() + p.getY());
 456                     screenX = location.getX();
 457                     sceneX = p.getX();
 458                     screenY = location.getY();
 459                 }
 460             }
 461 
 462             populateContextMenu();
 463             double menuWidth = contextMenu.prefWidth(-1);
 464             double menuX = screenX - (Properties.IS_TOUCH_SUPPORTED ? (menuWidth / 2) : 0);
 465             Screen currentScreen = com.sun.javafx.util.Utils.getScreenForPoint(screenX, 0);
 466             Rectangle2D bounds = currentScreen.getBounds();
 467 
 468             if (menuX < bounds.getMinX()) {
 469                 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX);
 470                 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX);
 471                 contextMenu.show(getNode(), bounds.getMinX(), screenY);
 472             } else if (screenX + menuWidth > bounds.getMaxX()) {
 473                 double leftOver = menuWidth - ( bounds.getMaxX() - screenX);
 474                 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX);
 475                 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX);
 476                 contextMenu.show(getNode(), screenX - leftOver, screenY);
 477             } else {
 478                 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", 0);
 479                 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", 0);
 480                 contextMenu.show(getNode(), menuX, screenY);
 481             }
 482         }
 483 
 484         e.consume();
 485     }
 486 
 487     @Override protected void setCaretAnimating(boolean play) {
 488         skin.setCaretAnimating(play);
 489     }
 490 
 491     protected void mouseDoubleClick(TextPosInfo hit) {
 492         final TextArea textArea = getNode();
 493         textArea.previousWord();
 494         if (isWindows()) {
 495             textArea.selectNextWord();
 496         } else {
 497             textArea.selectEndOfNextWord();
 498         }
 499     }
 500 
 501     protected void mouseTripleClick(TextPosInfo hit) {
 502         // select the line
 503         skin.moveCaret(TextUnit.PARAGRAPH, Direction.BEGINNING, false);
 504         skin.moveCaret(TextUnit.PARAGRAPH, Direction.END, true);
 505     }
 506 }