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