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