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, "TraverseOrInsertTab")); // 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 ("TraverseOrInsertTab".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 if ("TraverseOrInsertTab".equals(name)) {
 249                 // RT-40312: Non-editabe mode means traverse instead of insert.
 250                 name = "TraverseNext";
 251                 done = false;
 252             } else {
 253                 done = false;
 254             }
 255         }
 256 //            fnCaretAnim(true);
 257 
 258         if (!done) {
 259             super.callAction(name);
 260         }
 261     }
 262 
 263     private void insertNewLine() {
 264         TextArea textArea = getControl();
 265         textArea.replaceSelection("\n");
 266     }
 267 
 268     private void insertTab() {
 269         TextArea textArea = getControl();
 270         textArea.replaceSelection("\t");
 271     }
 272 
 273     @Override protected void deleteChar(boolean previous) {
 274         skin.deleteChar(previous);
 275     }
 276 
 277     @Override protected void deleteFromLineStart() {
 278         TextArea textArea = getControl();
 279         int end = textArea.getCaretPosition();
 280 
 281         if (end > 0) {
 282             lineStart(false, false);
 283             int start = textArea.getCaretPosition();
 284             if (end > start) {
 285                 replaceText(start, end, "");
 286             }
 287         }
 288     }
 289 
 290     private void lineStart(boolean select, boolean extendSelection) {
 291         skin.lineStart(select, extendSelection);
 292     }
 293 
 294     private void lineEnd(boolean select, boolean extendSelection) {
 295         skin.lineEnd(select, extendSelection);
 296     }
 297 
 298     protected void scrollCharacterToVisible(int index) {
 299         // TODO this method should be removed when TextAreaSkin
 300         // TODO is refactored to no longer need it.
 301         skin.scrollCharacterToVisible(index);
 302     }
 303 
 304     @Override protected void replaceText(int start, int end, String txt) {
 305         getControl().replaceText(start, end, txt);
 306     }
 307 
 308     /**
 309      * If the focus is gained via response to a mouse click, then we don't
 310      * want to select all the text even if selectOnFocus is true.
 311      */
 312     private boolean focusGainedByMouseClick = false; // TODO!!
 313     private boolean shiftDown = false;
 314     private boolean deferClick = false;
 315 
 316     @Override public void mousePressed(MouseEvent e) {
 317         TextArea textArea = getControl();
 318         super.mousePressed(e);
 319         // We never respond to events if disabled
 320         if (!textArea.isDisabled()) {
 321             // If the text field doesn't have focus, then we'll attempt to set
 322             // the focus and we'll indicate that we gained focus by a mouse
 323             // click, TODO which will then NOT honor the selectOnFocus variable
 324             // of the textInputControl
 325             if (!textArea.isFocused()) {
 326                 focusGainedByMouseClick = true;
 327                 textArea.requestFocus();
 328             }
 329 
 330             // stop the caret animation
 331             setCaretAnimating(false);
 332             // only if there is no selection should we see the caret
 333 //            setCaretOpacity(if (textInputControl.dot == textInputControl.mark) then 1.0 else 0.0);
 334 
 335             // if the primary button was pressed
 336             if (e.getButton() == MouseButton.PRIMARY && !(e.isMiddleButtonDown() || e.isSecondaryButtonDown())) {
 337                 HitInfo hit = skin.getIndex(e.getX(), e.getY());
 338                 int i = com.sun.javafx.scene.control.skin.Utils.getHitInsertionIndex(hit, textArea.textProperty().getValueSafe());
 339 //                 int i = skin.getInsertionPoint(e.getX(), e.getY());
 340                 final int anchor = textArea.getAnchor();
 341                 final int caretPosition = textArea.getCaretPosition();
 342                 if (e.getClickCount() < 2 &&
 343                     (e.isSynthesized() ||
 344                      (anchor != caretPosition &&
 345                       ((i > anchor && i < caretPosition) || (i < anchor && i > caretPosition))))) {
 346                     // if there is a selection, then we will NOT handle the
 347                     // press now, but will defer until the release. If you
 348                     // select some text and then press down, we change the
 349                     // caret and wait to allow you to drag the text (TODO).
 350                     // When the drag concludes, then we handle the click
 351 
 352                     deferClick = true;
 353                     // TODO start a timer such that after some millis we
 354                     // switch into text dragging mode, change the cursor
 355                     // to indicate the text can be dragged, etc.
 356                 } else if (!(e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown() || e.isShortcutDown())) {
 357                     switch (e.getClickCount()) {
 358                         case 1: skin.positionCaret(hit, false, false); break;
 359                         case 2: mouseDoubleClick(hit); break;
 360                         case 3: mouseTripleClick(hit); break;
 361                         default: // no-op
 362                     }
 363                 } else if (e.isShiftDown() && !(e.isControlDown() || e.isAltDown() || e.isMetaDown() || e.isShortcutDown()) && e.getClickCount() == 1) {
 364                     // didn't click inside the selection, so select
 365                     shiftDown = true;
 366                     // if we are on mac os, then we will accumulate the
 367                     // selection instead of just moving the dot. This happens
 368                     // by figuring out past which (dot/mark) are extending the
 369                     // selection, and set the mark to be the other side and
 370                     // the dot to be the new position.
 371                     // everywhere else we just move the dot.
 372                     if (isMac()) {
 373                         textArea.extendSelection(i);
 374                     } else {
 375                         skin.positionCaret(hit, true, false);
 376                     }
 377                 }
 378 //                 skin.setForwardBias(hit.isLeading());
 379 //                if (textInputControl.editable)
 380 //                    displaySoftwareKeyboard(true);
 381             }
 382             if (contextMenu.isShowing()) {
 383                 contextMenu.hide();
 384             }
 385         }
 386     }
 387 
 388     @Override public void mouseDragged(MouseEvent e) {
 389         final TextArea textArea = getControl();
 390         // we never respond to events if disabled, but we do notify any onXXX
 391         // event listeners on the control
 392         if (!textArea.isDisabled() && !e.isSynthesized()) {
 393             if (e.getButton() == MouseButton.PRIMARY &&
 394                     !(e.isMiddleButtonDown() || e.isSecondaryButtonDown() ||
 395                             e.isControlDown() || e.isAltDown() || e.isShiftDown() || e.isMetaDown())) {
 396                 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), true, false);
 397             }
 398         }
 399         deferClick = false;
 400     }
 401 
 402     @Override public void mouseReleased(final MouseEvent e) {
 403         final TextArea textArea = getControl();
 404         super.mouseReleased(e);
 405         // we never respond to events if disabled, but we do notify any onXXX
 406         // event listeners on the control
 407         if (!textArea.isDisabled()) {
 408             setCaretAnimating(false);
 409             if (deferClick) {
 410                 deferClick = false;
 411                 skin.positionCaret(skin.getIndex(e.getX(), e.getY()), shiftDown, false);
 412                 shiftDown = false;
 413             }
 414             setCaretAnimating(true);
 415         }
 416     }
 417 
 418     @Override public void contextMenuRequested(ContextMenuEvent e) {
 419         final TextArea textArea = getControl();
 420 
 421         if (contextMenu.isShowing()) {
 422             contextMenu.hide();
 423         } else if (textArea.getContextMenu() == null) {
 424             double screenX = e.getScreenX();
 425             double screenY = e.getScreenY();
 426             double sceneX = e.getSceneX();
 427 
 428             if (IS_TOUCH_SUPPORTED) {
 429                 Point2D menuPos;
 430                 if (textArea.getSelection().getLength() == 0) {
 431                     skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false, false);
 432                     menuPos = skin.getMenuPosition();
 433                 } else {
 434                     menuPos = skin.getMenuPosition();
 435                     if (menuPos != null && (menuPos.getX() <= 0 || menuPos.getY() <= 0)) {
 436                         skin.positionCaret(skin.getIndex(e.getX(), e.getY()), false, false);
 437                         menuPos = skin.getMenuPosition();
 438                     }
 439                 }
 440 
 441                 if (menuPos != null) {
 442                     Point2D p = getControl().localToScene(menuPos);
 443                     Scene scene = getControl().getScene();
 444                     Window window = scene.getWindow();
 445                     Point2D location = new Point2D(window.getX() + scene.getX() + p.getX(),
 446                                                    window.getY() + scene.getY() + p.getY());
 447                     screenX = location.getX();
 448                     sceneX = p.getX();
 449                     screenY = location.getY();
 450                 }
 451             }
 452 
 453             skin.populateContextMenu(contextMenu);
 454             double menuWidth = contextMenu.prefWidth(-1);
 455             double menuX = screenX - (IS_TOUCH_SUPPORTED ? (menuWidth / 2) : 0);
 456             Screen currentScreen = com.sun.javafx.util.Utils.getScreenForPoint(screenX, 0);
 457             Rectangle2D bounds = currentScreen.getBounds();
 458 
 459             if (menuX < bounds.getMinX()) {
 460                 getControl().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX);
 461                 getControl().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX);
 462                 contextMenu.show(getControl(), bounds.getMinX(), screenY);
 463             } else if (screenX + menuWidth > bounds.getMaxX()) {
 464                 double leftOver = menuWidth - ( bounds.getMaxX() - screenX);
 465                 getControl().getProperties().put("CONTEXT_MENU_SCREEN_X", screenX);
 466                 getControl().getProperties().put("CONTEXT_MENU_SCENE_X", sceneX);
 467                 contextMenu.show(getControl(), screenX - leftOver, screenY);
 468             } else {
 469                 getControl().getProperties().put("CONTEXT_MENU_SCREEN_X", 0);
 470                 getControl().getProperties().put("CONTEXT_MENU_SCENE_X", 0);
 471                 contextMenu.show(getControl(), menuX, screenY);
 472             }
 473         }
 474 
 475         e.consume();
 476     }
 477 
 478     @Override protected void setCaretAnimating(boolean play) {
 479         skin.setCaretAnimating(play);
 480     }
 481 
 482     protected void mouseDoubleClick(HitInfo hit) {
 483         final TextArea textArea = getControl();
 484         textArea.previousWord();
 485         if (isWindows()) {
 486             textArea.selectNextWord();
 487         } else {
 488             textArea.selectEndOfNextWord();
 489         }
 490     }
 491 
 492     protected void mouseTripleClick(HitInfo hit) {
 493         // select the line
 494         skin.paragraphStart(false, false);
 495         skin.paragraphEnd(false, isWindows(), true);
 496     }
 497 
 498     //    public function mouseWheelMove(e:MouseEvent):Void {
 499 //        def textBox = bind skin.control as TextBox;
 500 //        // we never respond to events if disabled, but we do notify any onXXX
 501 //        // event listeners on the control
 502 //        if (not textBox.disabled) {
 503 //            var rot = Math.abs(e.wheelRotation);
 504 //            while (rot > 0) {
 505 //                rot--;
 506 //                scrollText(e.wheelRotation > 0);
 507 //            }
 508 //        }
 509 //    }
 510 
 511 }