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 ContextMenu contextMenu;
 137     private TwoLevelFocusBehavior tlFocus;
 138 
 139     /**************************************************************************
 140      * Constructors                                                           *
 141      *************************************************************************/
 142 
 143     public TextAreaBehavior(final TextArea c) {
 144         super(c);
 145 
 146         contextMenu = new ContextMenu();
 147         if (Properties.IS_TOUCH_SUPPORTED) {
 148             contextMenu.getStyleClass().add("text-input-context-menu");
 149         }
 150 
 151         // some of the mappings are only valid when the control is editable, or
 152         // only on certain platforms, so we create the following predicates that filters out the mapping when the
 153         // control is not in the correct state / on the correct platform
 154         final Predicate<KeyEvent> validWhenEditable = e -> !c.isEditable();
 155         final Predicate<KeyEvent> validOnMac = e -> !PlatformUtil.isMac();
 156         final Predicate<KeyEvent> validOnWindows = e -> !PlatformUtil.isWindows();
 157         final Predicate<KeyEvent> validOnLinux = e -> !PlatformUtil.isLinux();
 158 
 159         // Add these bindings as a child input map, so they take precedence
 160         InputMap<TextArea> textAreaInputMap = new InputMap<>(c);
 161         textAreaInputMap.getMappings().addAll(
 162             keyMapping(HOME,      e -> lineStart(false)),
 163             keyMapping(END,       e -> lineEnd(false)),
 164             keyMapping(UP,        e -> skin.moveCaret(TextUnit.LINE, Direction.UP,   false)),
 165             keyMapping(DOWN,      e -> skin.moveCaret(TextUnit.LINE, Direction.DOWN, false)),
 166             keyMapping(PAGE_UP,   e -> skin.moveCaret(TextUnit.PAGE, Direction.UP,   false)),
 167             keyMapping(PAGE_DOWN, e -> skin.moveCaret(TextUnit.PAGE, Direction.DOWN, false)),
 168 
 169             keyMapping(new KeyBinding(HOME).shift(),      e -> lineStart(true)),
 170             keyMapping(new KeyBinding(END).shift(),       e -> lineEnd(true)),
 171             keyMapping(new KeyBinding(UP).shift(),        e -> skin.moveCaret(TextUnit.LINE, Direction.UP,   true)),
 172             keyMapping(new KeyBinding(DOWN).shift(),      e -> skin.moveCaret(TextUnit.LINE, Direction.DOWN, true)),
 173             keyMapping(new KeyBinding(PAGE_UP).shift(),   e -> skin.moveCaret(TextUnit.PAGE, Direction.UP,   true)),
 174             keyMapping(new KeyBinding(PAGE_DOWN).shift(), e -> skin.moveCaret(TextUnit.PAGE, Direction.DOWN, true)),
 175 
 176             // editing-only mappings
 177             keyMapping(new KeyBinding(ENTER), e -> insertNewLine(), validWhenEditable),
 178             keyMapping(new KeyBinding(TAB), e -> insertTab(), validWhenEditable)
 179         );
 180         addDefaultChildMap(getInputMap(), textAreaInputMap);
 181 
 182         // mac os specific mappings
 183         InputMap<TextArea> macOsInputMap = new InputMap<>(c);
 184         macOsInputMap.setInterceptor(e -> !PlatformUtil.isMac());
 185         macOsInputMap.getMappings().addAll(
 186             // Mac OS specific mappings
 187             keyMapping(new KeyBinding(LEFT).shortcut(),  e -> lineStart(false)),
 188             keyMapping(new KeyBinding(RIGHT).shortcut(), e -> lineEnd(false)),
 189             keyMapping(new KeyBinding(UP).shortcut(),    e -> c.home()),
 190             keyMapping(new KeyBinding(DOWN).shortcut(),  e -> c.end()),
 191 
 192             keyMapping(new KeyBinding(LEFT).shortcut().shift(),  e -> lineStart(true)),
 193             keyMapping(new KeyBinding(RIGHT).shortcut().shift(), e -> lineEnd(true)),
 194             keyMapping(new KeyBinding(UP).shortcut().shift(),    e -> selectHomeExtend()),
 195             keyMapping(new KeyBinding(DOWN).shortcut().shift(),  e -> selectEndExtend()),
 196 
 197             keyMapping(new KeyBinding(UP).alt(),           e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.UP,   false)),
 198             keyMapping(new KeyBinding(DOWN).alt(),         e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.DOWN, false)),
 199             keyMapping(new KeyBinding(UP).alt().shift(),   e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.UP,   true)),
 200             keyMapping(new KeyBinding(DOWN).alt().shift(), e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.DOWN, true))
 201         );
 202         addDefaultChildMap(textAreaInputMap, macOsInputMap);
 203 
 204         // windows / linux specific mappings
 205         InputMap<TextArea> nonMacOsInputMap = new InputMap<>(c);
 206         nonMacOsInputMap.setInterceptor(e -> PlatformUtil.isMac());
 207         nonMacOsInputMap.getMappings().addAll(
 208             keyMapping(new KeyBinding(UP).ctrl(),           e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.UP,   false)),
 209             keyMapping(new KeyBinding(DOWN).ctrl(),         e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.DOWN, false)),
 210             keyMapping(new KeyBinding(UP).ctrl().shift(),   e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.UP,   true)),
 211             keyMapping(new KeyBinding(DOWN).ctrl().shift(), e -> skin.moveCaret(TextUnit.PARAGRAPH, Direction.DOWN, true))
 212         );
 213         addDefaultChildMap(textAreaInputMap, nonMacOsInputMap);
 214 
 215         addKeyPadMappings(textAreaInputMap);
 216 
 217         // Register for change events
 218         c.focusedProperty().addListener(new ChangeListener<Boolean>() {
 219             @Override
 220             public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
 221                 // NOTE: The code in this method is *almost* and exact copy of what is in TextFieldBehavior.
 222                 // The only real difference is that TextFieldBehavior selects all the text when the control
 223                 // receives focus (when not gained by mouse click), whereas TextArea doesn't, and also the
 224                 // TextArea doesn't lose selection on focus lost, whereas the TextField does.
 225                 final TextArea textArea = getNode();
 226                 if (textArea.isFocused()) {
 227                     if (PlatformUtil.isIOS()) {
 228                         // Special handling of focus on iOS is required to allow to
 229                         // control native keyboard, because native keyboard is popped-up only when native
 230                         // text component gets focus. When we have JFX keyboard we can remove this code
 231                         final Bounds bounds = textArea.getBoundsInParent();
 232                         double w = bounds.getWidth();
 233                         double h = bounds.getHeight();
 234                         Affine3D trans = TextFieldBehavior.calculateNodeToSceneTransform(textArea);
 235                         String text = textArea.textProperty().getValueSafe();
 236 
 237                         // we need to display native text input component on the place where JFX component is drawn
 238                         // all parameters needed to do that are passed to native impl. here
 239                         textArea.getScene().getWindow().impl_getPeer().requestInput(text, TextFieldBehavior.TextInputTypes.TEXT_AREA.ordinal(), w, h,
 240                                 trans.getMxx(), trans.getMxy(), trans.getMxz(), trans.getMxt(),
 241                                 trans.getMyx(), trans.getMyy(), trans.getMyz(), trans.getMyt(),
 242                                 trans.getMzx(), trans.getMzy(), trans.getMzz(), trans.getMzt());
 243                     }
 244                     if (!focusGainedByMouseClick) {
 245                         setCaretAnimating(true);
 246                     }
 247                 } else {
 248 //                    skin.hideCaret();
 249                     if (PlatformUtil.isIOS() && textArea.getScene() != null) {
 250                         // releasing the focus => we need to hide the native component and also native keyboard
 251                         textArea.getScene().getWindow().impl_getPeer().releaseInput();
 252                     }
 253                     focusGainedByMouseClick = false;
 254                     setCaretAnimating(false);
 255                 }
 256             }
 257         });
 258 
 259         // Only add this if we're on an embedded platform that supports 5-button navigation
 260         if (Utils.isTwoLevelFocus()) {
 261             tlFocus = new TwoLevelFocusBehavior(c); // needs to be last.
 262         }
 263     }
 264 
 265     @Override public void dispose() {
 266         if (tlFocus != null) tlFocus.dispose();
 267         super.dispose();
 268     }
 269 
 270     // An unholy back-reference!
 271     public void setTextAreaSkin(TextAreaSkin skin) {
 272         this.skin = skin;
 273     }
 274 
 275     private void insertNewLine() {
 276         setEditing(true);
 277         getNode().replaceSelection("\n");
 278         setEditing(false);
 279     }
 280 
 281     private void insertTab() {
 282         setEditing(true);
 283         getNode().replaceSelection("\t");
 284         setEditing(false);
 285     }
 286 
 287     @Override protected void deleteChar(boolean previous) {
 288         if (previous) {
 289             getNode().deletePreviousChar();
 290         } else {
 291             getNode().deleteNextChar();
 292         }
 293     }
 294 
 295     @Override protected void deleteFromLineStart() {
 296         TextArea textArea = getNode();
 297         int end = textArea.getCaretPosition();
 298 
 299         if (end > 0) {
 300             lineStart(false);
 301             int start = textArea.getCaretPosition();
 302             if (end > start) {
 303                 replaceText(start, end, "");
 304             }
 305         }
 306     }
 307 
 308     private void lineStart(boolean select) {
 309         skin.moveCaret(TextUnit.LINE, Direction.BEGINNING, select);
 310     }
 311 
 312     private void lineEnd(boolean select) {
 313         skin.moveCaret(TextUnit.LINE, Direction.END, select);
 314     }
 315 
 316     @Override protected void replaceText(int start, int end, String txt) {
 317         getNode().replaceText(start, end, txt);
 318     }
 319 
 320     /**
 321      * If the focus is gained via response to a mouse click, then we don't
 322      * want to select all the text even if selectOnFocus is true.
 323      */
 324     private boolean focusGainedByMouseClick = false; // TODO!!
 325     private boolean shiftDown = false;
 326     private boolean deferClick = false;
 327 
 328     @Override public void mousePressed(MouseEvent e) {
 329         TextArea textArea = getNode();
 330         // We never respond to events if disabled
 331         if (!textArea.isDisabled()) {
 332             // If the text field doesn't have focus, then we'll attempt to set
 333             // the focus and we'll indicate that we gained focus by a mouse
 334             // click, TODO which will then NOT honor the selectOnFocus variable
 335             // of the textInputControl
 336             if (!textArea.isFocused()) {
 337                 focusGainedByMouseClick = true;
 338                 textArea.requestFocus();
 339             }
 340 
 341             // stop the caret animation
 342             setCaretAnimating(false);
 343             // only if there is no selection should we see the caret
 344 //            setCaretOpacity(if (textInputControl.dot == textInputControl.mark) then 1.0 else 0.0);
 345 
 346             // if the primary button was pressed
 347             if (e.getButton() == MouseButton.PRIMARY && !(e.isMiddleButtonDown() || e.isSecondaryButtonDown())) {
 348                 TextPosInfo hit = skin.getIndex(e.getX(), e.getY());
 349                 int i = Utils.getHitInsertionIndex(hit, textArea.textProperty().getValueSafe());
 350 //                 int i = skin.getInsertionPoint(e.getX(), e.getY());
 351                 final int anchor = textArea.getAnchor();
 352                 final int caretPosition = textArea.getCaretPosition();
 353                 if (e.getClickCount() < 2 &&
 354                     (e.isSynthesized() ||
 355                      (anchor != caretPosition &&
 356                       ((i > anchor && i < caretPosition) || (i < anchor && i > caretPosition))))) {
 357                     // if there is a selection, then we will NOT handle the
 358                     // press now, but will defer until the release. If you
 359                     // select some text and then press down, we change the
 360                     // caret and wait to allow you to drag the text (TODO).
 361                     // When the drag concludes, then we handle the click
 362 
 363                     deferClick = true;
 364                     // TODO start a timer such that after some millis we
 365                     // switch into text dragging mode, change the cursor
 366                     // to indicate the text can be dragged, etc.
 367                 } else if (!(e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown() || e.isShortcutDown())) {
 368                     switch (e.getClickCount()) {
 369                         case 1: skin.positionCaret(hit, false); break;
 370                         case 2: mouseDoubleClick(hit); break;
 371                         case 3: mouseTripleClick(hit); break;
 372                         default: // no-op
 373                     }
 374                 } else if (e.isShiftDown() && !(e.isControlDown() || e.isAltDown() || e.isMetaDown() || e.isShortcutDown()) && e.getClickCount() == 1) {
 375                     // didn't click inside the selection, so select
 376                     shiftDown = true;
 377                     // if we are on mac os, then we will accumulate the
 378                     // selection instead of just moving the dot. This happens
 379                     // by figuring out past which (dot/mark) are extending the
 380                     // selection, and set the mark to be the other side and
 381                     // the dot to be the new position.
 382                     // everywhere else we just move the dot.
 383                     if (isMac()) {
 384                         textArea.extendSelection(i);
 385                     } else {
 386                         skin.positionCaret(hit, true);
 387                     }
 388                 }
 389 //                 skin.setForwardBias(hit.isLeading());
 390 //                if (textInputControl.editable)
 391 //                    displaySoftwareKeyboard(true);
 392             }
 393             if (contextMenu.isShowing()) {
 394                 contextMenu.hide();
 395             }
 396         }
 397     }
 398 
 399     @Override public void mouseDragged(MouseEvent e) {
 400         final TextArea textArea = getNode();
 401         // we never respond to events if disabled, but we do notify any onXXX
 402         // event listeners on the control
 403         if (!textArea.isDisabled() && !e.isSynthesized()) {
 404             if (e.getButton() == MouseButton.PRIMARY &&
 405                     !(e.isMiddleButtonDown() || e.isSecondaryButtonDown() ||
 406                             e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown())) {
 407                 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), true);
 408             }
 409         }
 410         deferClick = false;
 411     }
 412 
 413     @Override public void mouseReleased(final MouseEvent e) {
 414         final TextArea textArea = getNode();
 415         // we never respond to events if disabled, but we do notify any onXXX
 416         // event listeners on the control
 417         if (!textArea.isDisabled()) {
 418             setCaretAnimating(false);
 419             if (deferClick) {
 420                 deferClick = false;
 421                 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), shiftDown);
 422                 shiftDown = false;
 423             }
 424             setCaretAnimating(true);
 425         }
 426     }
 427 
 428     @Override public void contextMenuRequested(ContextMenuEvent e) {
 429         final TextArea textArea = getNode();
 430 
 431         if (contextMenu.isShowing()) {
 432             contextMenu.hide();
 433         } else if (textArea.getContextMenu() == null) {
 434             double screenX = e.getScreenX();
 435             double screenY = e.getScreenY();
 436             double sceneX = e.getSceneX();
 437 
 438             if (Properties.IS_TOUCH_SUPPORTED) {
 439                 Point2D menuPos;
 440                 if (textArea.getSelection().getLength() == 0) {
 441                     skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false);
 442                     menuPos = skin.getMenuPosition();
 443                 } else {
 444                     menuPos = skin.getMenuPosition();
 445                     if (menuPos != null && (menuPos.getX() <= 0 || menuPos.getY() <= 0)) {
 446                         skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false);
 447                         menuPos = skin.getMenuPosition();
 448                     }
 449                 }
 450 
 451                 if (menuPos != null) {
 452                     Point2D p = getNode().localToScene(menuPos);
 453                     Scene scene = getNode().getScene();
 454                     Window window = scene.getWindow();
 455                     Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(),
 456                                                    window.getY() + scene.getY() + p.getY());
 457                     screenX = location.getX();
 458                     sceneX = p.getX();
 459                     screenY = location.getY();
 460                 }
 461             }
 462 
 463             populateContextMenu(contextMenu);
 464             double menuWidth = contextMenu.prefWidth(-1);
 465             double menuX = screenX - (Properties.IS_TOUCH_SUPPORTED ? (menuWidth / 2) : 0);
 466             Screen currentScreen = com.sun.javafx.util.Utils.getScreenForPoint(screenX, 0);
 467             Rectangle2D bounds = currentScreen.getBounds();
 468 
 469             if (menuX < bounds.getMinX()) {
 470                 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX);
 471                 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX);
 472                 contextMenu.show(getNode(), bounds.getMinX(), screenY);
 473             } else if (screenX + menuWidth > bounds.getMaxX()) {
 474                 double leftOver = menuWidth - ( bounds.getMaxX() - screenX);
 475                 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX);
 476                 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX);
 477                 contextMenu.show(getNode(), screenX - leftOver, screenY);
 478             } else {
 479                 getNode().getProperties().put("CONTEXT_MENU_SCREEN_X", 0);
 480                 getNode().getProperties().put("CONTEXT_MENU_SCENE_X", 0);
 481                 contextMenu.show(getNode(), menuX, screenY);
 482             }
 483         }
 484 
 485         e.consume();
 486     }
 487 
 488     @Override protected void setCaretAnimating(boolean play) {
 489         skin.setCaretAnimating(play);
 490     }
 491 
 492     protected void mouseDoubleClick(TextPosInfo hit) {
 493         final TextArea textArea = getNode();
 494         textArea.previousWord();
 495         if (isWindows()) {
 496             textArea.selectNextWord();
 497         } else {
 498             textArea.selectEndOfNextWord();
 499         }
 500     }
 501 
 502     protected void mouseTripleClick(TextPosInfo hit) {
 503         // select the line
 504         skin.moveCaret(TextUnit.PARAGRAPH, Direction.BEGINNING, false);
 505         skin.moveCaret(TextUnit.PARAGRAPH, Direction.END, true);
 506     }
 507 }