1 /*
   2  * Copyright (c) 2010, 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.skin;
  27 
  28 import javafx.animation.Animation;
  29 import javafx.animation.Interpolator;
  30 import javafx.animation.KeyFrame;
  31 import javafx.animation.KeyValue;
  32 import javafx.animation.Timeline;
  33 import javafx.application.Platform;
  34 import javafx.beans.InvalidationListener;
  35 import javafx.beans.Observable;
  36 import javafx.beans.property.DoubleProperty;
  37 import javafx.beans.property.SimpleDoubleProperty;
  38 import javafx.event.EventHandler;
  39 import javafx.event.EventTarget;
  40 import javafx.geometry.Bounds;
  41 import javafx.geometry.HPos;
  42 import javafx.geometry.Point2D;
  43 import javafx.geometry.Rectangle2D;
  44 import javafx.geometry.VPos;
  45 import javafx.scene.Node;
  46 import javafx.scene.Parent;
  47 import javafx.scene.control.TextInputControl;
  48 import javafx.scene.control.TextField;
  49 import javafx.scene.control.TextArea;
  50 import javafx.scene.control.ComboBoxBase;
  51 import javafx.scene.Scene;
  52 import javafx.scene.input.InputEvent;
  53 import javafx.scene.input.KeyCode;
  54 import javafx.scene.input.KeyEvent;
  55 import javafx.scene.input.MouseEvent;
  56 import javafx.scene.input.MouseButton;
  57 import javafx.scene.layout.Region;
  58 import javafx.scene.text.Text;
  59 import javafx.stage.Popup;
  60 import javafx.stage.Window;
  61 import javafx.util.Duration;
  62 import java.io.BufferedReader;
  63 import java.io.InputStream;
  64 import java.io.InputStreamReader;
  65 import java.util.ArrayList;
  66 import java.util.Collections;
  67 import java.util.List;
  68 import java.util.Locale;
  69 import java.util.HashMap;
  70 import com.sun.javafx.scene.control.behavior.BehaviorBase;
  71 import static javafx.scene.input.MouseEvent.MOUSE_PRESSED;
  72 import static javafx.scene.input.TouchEvent.TOUCH_PRESSED;
  73 import static javafx.scene.layout.Region.USE_PREF_SIZE;
  74 import java.security.AccessController;
  75 import java.security.PrivilegedAction;
  76 
  77 
  78 public class FXVKSkin extends BehaviorSkinBase<FXVK, BehaviorBase<FXVK>> {
  79 
  80     private static final int GAP = 6;
  81 
  82     private List<List<Key>> currentBoard;
  83     private static HashMap<String, List<List<Key>>> boardMap = new HashMap<String, List<List<Key>>>();
  84     private int numCols;
  85 
  86     private boolean capsDown = false;
  87     private boolean shiftDown = false;
  88     private boolean isSymbol = false;
  89     long lastTime = -1L;
  90 
  91     void clearShift() {
  92         if (shiftDown && !capsDown) {
  93             shiftDown = false;
  94             updateKeys();
  95         }
  96         lastTime = -1L;
  97     }
  98 
  99     void pressShift() {
 100         long time = System.currentTimeMillis();
 101         
 102         //potential for a shift lock
 103         if (shiftDown && !capsDown) {
 104             if (lastTime > 0L && time - lastTime < 400L) {
 105                 //set caps lock
 106                 shiftDown = false;
 107                 capsDown =  true;
 108             } else {
 109                 //set normal
 110                 shiftDown = false;
 111                 capsDown =  false;
 112             }
 113         } else if (!shiftDown && !capsDown) {
 114             // set shift
 115             shiftDown=true;
 116         } else {
 117             //set to normal
 118             shiftDown = false;
 119             capsDown =  false;
 120         }
 121         
 122         updateKeys();
 123         lastTime = time;
 124     }
 125 
 126     void clearSymbolABC() {
 127         isSymbol = false;
 128         updateKeys();
 129     }
 130 
 131     void pressSymbolABC() {
 132         isSymbol = !isSymbol;
 133         updateKeys();
 134     }
 135 
 136     void clearStateKeys() {
 137         capsDown = false;
 138         shiftDown = false;
 139         isSymbol = false;
 140         lastTime = -1L;
 141         updateKeys();
 142     }
 143 
 144     private void updateKeys() {
 145         for (List<Key> row : currentBoard) {
 146             for (Key key : row) {
 147                 key.update(capsDown, shiftDown, isSymbol);
 148             }
 149         }
 150     }
 151 
 152     private static Popup vkPopup;
 153     private static Popup secondaryPopup;
 154     private static FXVK primaryVK;
 155 
 156     private static Timeline slideInTimeline = new Timeline();
 157     private static Timeline slideOutTimeline = new Timeline();
 158     private static boolean hideAfterSlideOut = false;
 159 
 160     private static FXVK secondaryVK;
 161     private static Timeline secondaryVKDelay;
 162     private static CharKey secondaryVKKey;
 163     private static TextInputKey repeatKey;
 164 
 165     private static Timeline repeatInitialDelay;
 166     private static Timeline repeatSubsequentDelay;
 167 
 168     // key repeat initial delay (ms)
 169     private static double KEY_REPEAT_DELAY = 400;
 170     private static double KEY_REPEAT_DELAY_MIN = 100;
 171     private static double KEY_REPEAT_DELAY_MAX = 1000;
 172 
 173     // key repeat rate (cps)
 174     private static double KEY_REPEAT_RATE = 25;
 175     private static double KEY_REPEAT_RATE_MIN = 2;
 176     private static double KEY_REPEAT_RATE_MAX = 50;
 177 
 178     private Node attachedNode;
 179     private String vkType = null;
 180 
 181     FXVK fxvk;
 182 
 183     static final double VK_HEIGHT = 243;
 184     static final double VK_SLIDE_MILLIS = 250;
 185     static final double PREF_PORTRAIT_KEY_WIDTH = 40;
 186     static final double PREF_KEY_HEIGHT = 56;
 187 
 188     static boolean vkAdjustWindow = false;
 189     static boolean vkLookup = false;
 190 
 191     static {
 192         AccessController.doPrivileged((PrivilegedAction<Void>) () -> {
 193             String s = System.getProperty("com.sun.javafx.vk.adjustwindow");
 194             if (s != null) {
 195                 vkAdjustWindow = Boolean.valueOf(s);
 196             }
 197             s = System.getProperty("com.sun.javafx.sqe.vk.lookup");
 198             if (s != null) {
 199                 vkLookup = Boolean.valueOf(s);
 200             }
 201             s = System.getProperty("com.sun.javafx.virtualKeyboard.backspaceRepeatDelay");
 202             if (s != null) {
 203                 Double delay = Double.valueOf(s);
 204                 KEY_REPEAT_DELAY = Math.min(Math.max(delay, KEY_REPEAT_DELAY_MIN), KEY_REPEAT_DELAY_MAX);
 205             }
 206             s = System.getProperty("com.sun.javafx.virtualKeyboard.backspaceRepeatRate");
 207             if (s != null) {
 208                 Double rate = Double.valueOf(s);
 209                 if (rate <= 0) {
 210                     //disable key repeat
 211                     KEY_REPEAT_RATE = 0;
 212                 } else {
 213                     KEY_REPEAT_RATE = Math.min(Math.max(rate, KEY_REPEAT_RATE_MIN), KEY_REPEAT_RATE_MAX);
 214                 }
 215             }
 216             return null;
 217         });
 218     }    
 219     
 220     // Proxy for read-only Window.yProperty() so we can animate.
 221     private static DoubleProperty winY = new SimpleDoubleProperty();
 222     static {
 223         winY.addListener(valueModel -> {
 224             if (vkPopup != null) {
 225                 vkPopup.setY(winY.get());
 226             }
 227         });
 228     }
 229 
 230     private static void startSlideIn() {
 231         slideOutTimeline.stop();
 232         slideInTimeline.playFromStart();
 233     }
 234 
 235     private static void startSlideOut(boolean doHide) {
 236         hideAfterSlideOut = doHide;
 237         slideInTimeline.stop();
 238         slideOutTimeline.playFromStart();
 239     }
 240 
 241     private void adjustWindowPosition(final Node node) {
 242         if ( !(node instanceof TextInputControl) ) {
 243             return;
 244         }
 245 
 246         // attached node y position in window coordinates
 247         double inputControlMinY = node.localToScene(0.0, 0.0).getY() + node.getScene().getY();
 248         double inputControlHeight = ((TextInputControl) node).getHeight();
 249         double inputControlMaxY = inputControlMinY + inputControlHeight; 
 250 
 251         double screenHeight =
 252             com.sun.javafx.util.Utils.getScreen(node).getBounds().getHeight();
 253         double visibleAreaMaxY = screenHeight - VK_HEIGHT;
 254 
 255         double inputLineCenterY = 0.0;
 256         double inputLineBottomY = 0.0;
 257         double newWindowYPos = 0.0;
 258         double screenTopOffset = 10.0;
 259 
 260         if (node instanceof TextField) {
 261             inputLineCenterY = inputControlMinY + inputControlHeight / 2;
 262             inputLineBottomY = inputControlMaxY;
 263             //check for combo box
 264             Parent parent = attachedNode.getParent();
 265             if (parent instanceof ComboBoxBase) {
 266                 //combo box
 267                 // position near screen top
 268                 newWindowYPos = Math.min(screenTopOffset - inputControlMinY, 0);
 269             } else {
 270                 // position at center of visible screen area
 271                 newWindowYPos = Math.min(visibleAreaMaxY / 2 - inputLineCenterY, 0);
 272             }
 273         } else if (node instanceof TextArea) {
 274             TextAreaSkin textAreaSkin = (TextAreaSkin)((TextArea)node).getSkin();
 275             Bounds caretBounds = textAreaSkin.getCaretBounds();
 276             double caretMinY = caretBounds.getMinY();
 277             double caretMaxY = caretBounds.getMaxY();
 278             inputLineCenterY = inputControlMinY + ( caretMinY + caretMaxY ) / 2;
 279             inputLineBottomY = inputControlMinY + caretMaxY;
 280 
 281             if (inputControlHeight < visibleAreaMaxY) {
 282                 // position at center of visible screen area
 283                 newWindowYPos = visibleAreaMaxY / 2 - (inputControlMinY + inputControlHeight / 2);
 284             } else {
 285                 // position the line containing the caret at center of visible screen area
 286                 newWindowYPos = visibleAreaMaxY / 2 - inputLineCenterY;
 287             }
 288             newWindowYPos = Math.min(newWindowYPos, 0);
 289 
 290         } else {
 291             inputLineCenterY = inputControlMinY + inputControlHeight / 2;
 292             inputLineBottomY = inputControlMaxY;
 293             // position at center of visible screen area
 294             newWindowYPos = Math.min(visibleAreaMaxY / 2 - inputLineCenterY, 0);
 295         }
 296        
 297         Window w = node.getScene().getWindow();
 298         if (origWindowYPos + inputLineBottomY > visibleAreaMaxY) {
 299             w.setY(newWindowYPos);
 300         } else {
 301             w.setY(origWindowYPos);
 302         }
 303     }
 304 
 305     private void saveWindowPosition(final Node node) {
 306         Window w = node.getScene().getWindow();
 307         origWindowYPos = w.getY();
 308     }
 309 
 310     private void restoreWindowPosition(final Node node) {
 311         if (node != null) {
 312             Scene scene = node.getScene();
 313             if (scene != null) {
 314                 Window window = scene.getWindow();
 315                 if (window != null) {
 316                     window.setY(origWindowYPos);
 317                 }
 318             }
 319         }
 320     }
 321 
 322     EventHandler<InputEvent> unHideEventHandler;
 323 
 324     private boolean isVKHidden = false;
 325     private Double origWindowYPos = null;
 326     
 327     private void registerUnhideHandler(final Node node) {
 328         if (unHideEventHandler == null) {
 329             unHideEventHandler = event -> {
 330                 if (attachedNode != null && isVKHidden) {
 331                     double screenHeight = com.sun.javafx.util.Utils.getScreen(attachedNode).getBounds().getHeight();
 332                     if (fxvk.getHeight() > 0 && (vkPopup.getY() > screenHeight - fxvk.getHeight())) {
 333                         if (slideInTimeline.getStatus() != Animation.Status.RUNNING) {
 334                             startSlideIn();
 335                             if (vkAdjustWindow) {
 336                                 adjustWindowPosition(attachedNode);
 337                             }
 338                         }
 339                     }
 340                 }
 341                 isVKHidden = false;
 342             };
 343         }
 344         node.addEventHandler(TOUCH_PRESSED, unHideEventHandler);
 345         node.addEventHandler(MOUSE_PRESSED, unHideEventHandler);
 346     }
 347 
 348     private void unRegisterUnhideHandler(Node node) {
 349         if (unHideEventHandler != null) {
 350             node.removeEventHandler(TOUCH_PRESSED, unHideEventHandler);
 351             node.removeEventHandler(MOUSE_PRESSED, unHideEventHandler);
 352         }
 353     }
 354 
 355     private String getNodeVKType(Node node) {
 356         Object typeValue = node.getProperties().get(FXVK.VK_TYPE_PROP_KEY);
 357         String typeStr = null;
 358         if (typeValue instanceof String) {
 359             typeStr = ((String)typeValue).toLowerCase(Locale.ROOT);
 360         }
 361         return (typeStr != null ? typeStr : "text");
 362     }
 363 
 364     private void updateKeyboardType(Node node) {
 365         String oldType = vkType;
 366         vkType = getNodeVKType(node);
 367         //VK type changed, rebuild
 368         if ( oldType == null || !vkType.equals(oldType) ) {
 369             rebuildPrimaryVK(vkType);
 370         }
 371     }
 372 
 373     private void closeSecondaryVK() {
 374         if (secondaryVK != null) {
 375             secondaryVK.setAttachedNode(null);
 376             secondaryPopup.hide();
 377         }
 378     }
 379 
 380     private void setupPrimaryVK() {
 381         fxvk.setFocusTraversable(false);
 382         fxvk.setVisible(true);
 383 
 384         // init popup window and slide animations
 385         if (vkPopup == null) {
 386             vkPopup = new Popup();
 387             vkPopup.setAutoFix(false);
 388         }
 389         vkPopup.getContent().setAll(fxvk);
 390 
 391         double screenHeight =
 392             com.sun.javafx.util.Utils.getScreen(fxvk).getBounds().getHeight();
 393         double width = com.sun.javafx.util.Utils.getScreen(fxvk).getBounds().getWidth();
 394 
 395         //Setup VK slide animations
 396         slideInTimeline.getKeyFrames().setAll(
 397             new KeyFrame(Duration.millis(VK_SLIDE_MILLIS),
 398                          new KeyValue(winY, screenHeight - VK_HEIGHT,
 399                                       Interpolator.EASE_BOTH)));
 400         slideOutTimeline.getKeyFrames().setAll(
 401             new KeyFrame(Duration.millis(VK_SLIDE_MILLIS),
 402                     event -> {
 403                         if (hideAfterSlideOut && vkPopup.isShowing()) {
 404                             vkPopup.hide();
 405                         }
 406                     },
 407                 new KeyValue(winY, screenHeight, Interpolator.EASE_BOTH)));
 408 
 409         //Set VK size
 410         fxvk.setPrefWidth(width);
 411         fxvk.setMinWidth(USE_PREF_SIZE);
 412         fxvk.setMaxWidth(USE_PREF_SIZE);
 413 
 414         fxvk.setPrefHeight(VK_HEIGHT);
 415         fxvk.setMinHeight(USE_PREF_SIZE);
 416 
 417 
 418         //set up long-press triger for secondary VK
 419         if (secondaryVKDelay == null) {
 420             secondaryVKDelay = new Timeline();
 421         }
 422         KeyFrame kf = new KeyFrame(Duration.millis(500), event -> {
 423             if (secondaryVKKey != null) {
 424                 showSecondaryVK(secondaryVKKey);
 425             }
 426         });
 427         secondaryVKDelay.getKeyFrames().setAll(kf);
 428 
 429         //Setup key repeat animations
 430         if (KEY_REPEAT_RATE > 0) {
 431             repeatInitialDelay = new Timeline(new KeyFrame(
 432                     Duration.millis(KEY_REPEAT_DELAY),
 433                     event -> {
 434                         //fire current key
 435                         repeatKey.sendKeyEvents();
 436                         //Start repeat animation
 437                         repeatSubsequentDelay.playFromStart();
 438                     }
 439             ));
 440             repeatSubsequentDelay = new Timeline(new KeyFrame(
 441                     Duration.millis(1000.0 / KEY_REPEAT_RATE),
 442                     event -> {
 443                         //fire current key
 444                         repeatKey.sendKeyEvents();
 445                     }
 446             ));
 447             repeatSubsequentDelay.setCycleCount(Animation.INDEFINITE);
 448         }
 449     }
 450 
 451     void prerender(Node node) {
 452         if (fxvk != primaryVK) {
 453             return;
 454         }
 455         
 456         //Preload all boards
 457         loadBoard("text");
 458         loadBoard("numeric");
 459         loadBoard("url");
 460         loadBoard("email");
 461 
 462         updateKeyboardType(node);
 463         fxvk.setVisible(true);
 464 
 465         if (!vkPopup.isShowing()) {
 466             Rectangle2D screenBounds =
 467                 com.sun.javafx.util.Utils.getScreen(node).getBounds();
 468 
 469             vkPopup.setX((screenBounds.getWidth() - fxvk.prefWidth(-1)) / 2);
 470             winY.set(screenBounds.getHeight());
 471             vkPopup.show(node.getScene().getWindow());
 472         }             
 473     }
 474 
 475     public FXVKSkin(final FXVK fxvk) {
 476         super(fxvk, new BehaviorBase<>(fxvk, Collections.emptyList()));
 477         this.fxvk = fxvk;
 478         if (fxvk == FXVK.vk) {
 479             primaryVK = fxvk;
 480         }
 481 
 482         if (fxvk == primaryVK) {
 483             setupPrimaryVK();
 484         }
 485 
 486         fxvk.attachedNodeProperty().addListener(new InvalidationListener() {
 487             @Override public void invalidated(Observable valueModel) {
 488                 Node oldNode = attachedNode;
 489                 attachedNode = fxvk.getAttachedNode();
 490                 if (fxvk != primaryVK) {
 491                     return;
 492                 }
 493                 
 494                 closeSecondaryVK();
 495                 
 496                 if (attachedNode != null) {
 497                     if (oldNode != null) {
 498                         unRegisterUnhideHandler(oldNode);
 499                     }
 500                     registerUnhideHandler(attachedNode);
 501                     updateKeyboardType(attachedNode);
 502 
 503                     //owner window has changed so hide VK and show with new owner
 504                     if (oldNode == null || oldNode.getScene() == null || oldNode.getScene().getWindow() != attachedNode.getScene().getWindow()) {
 505                         if (vkPopup.isShowing()) {
 506                             vkPopup.hide();
 507                         } else {
 508                         }
 509                     }
 510 
 511                     if (!vkPopup.isShowing()) {
 512                         Rectangle2D screenBounds =
 513                             com.sun.javafx.util.Utils.getScreen(attachedNode).getBounds();
 514 
 515                         vkPopup.setX((screenBounds.getWidth() - fxvk.prefWidth(-1)) / 2);
 516                         if (oldNode == null || isVKHidden) {
 517                             //position off screen
 518                             winY.set(screenBounds.getHeight());
 519                         } else {
 520                             //position on screen (no slide in)
 521                             winY.set(screenBounds.getHeight() - VK_HEIGHT);
 522                         }
 523                         vkPopup.show(attachedNode.getScene().getWindow());
 524                     }             
 525 
 526                     if (oldNode == null || isVKHidden) {
 527                         startSlideIn();
 528                     }
 529                         
 530                     if (vkAdjustWindow) {
 531                         //update previous window position only if moving from non-input control node or window has changed.
 532                         if (oldNode == null || oldNode.getScene() == null 
 533                             || oldNode.getScene().getWindow() != attachedNode.getScene().getWindow()) {
 534                             saveWindowPosition(attachedNode);
 535                         }
 536                         // Move window containing input node
 537                         adjustWindowPosition(attachedNode);
 538                     }
 539                 } else { // attachedNode == null
 540                     if (oldNode != null) {
 541                         unRegisterUnhideHandler(oldNode);
 542                     }
 543                     startSlideOut(true);
 544                     // Restore window position
 545                     if (vkAdjustWindow) {
 546                         restoreWindowPosition(oldNode);
 547                     }
 548                 }
 549                 isVKHidden = false;
 550             }
 551         });
 552     }
 553 
 554     /**
 555      * builds secondary (long-press) VK
 556      */
 557     private void rebuildSecondaryVK() {
 558         if (secondaryVK.chars == null) {
 559         } else {
 560             int nKeys = secondaryVK.chars.length;
 561             int nRows = (int)Math.floor(Math.sqrt(Math.max(1, nKeys - 2)));
 562             int nKeysPerRow = (int)Math.ceil(nKeys / (double)nRows);
 563 
 564             Key tmpKey;
 565             List<List<Key>> rows = new ArrayList<List<Key>>(2);
 566 
 567             for (int i = 0; i < nRows; i++) {
 568                 int start = i * nKeysPerRow;
 569                 int end = Math.min(start + nKeysPerRow, nKeys);
 570                 if (start >= end) 
 571                     break;
 572                     
 573                 List<Key> keys = new ArrayList<Key>(nKeysPerRow);
 574                 for (int j = start; j < end; j++) {
 575                     tmpKey = new CharKey(secondaryVK.chars[j], null, null);
 576                     tmpKey.col= (j - start) * 2;
 577                     tmpKey.colSpan = 2;
 578                     for (String sc : tmpKey.getStyleClass()) {
 579                         tmpKey.text.getStyleClass().add(sc + "-text");
 580                         tmpKey.altText.getStyleClass().add(sc + "-alttext");
 581                         tmpKey.icon.getStyleClass().add(sc + "-icon");
 582                     }
 583                     if (secondaryVK.chars[j] != null && secondaryVK.chars[j].length() > 1) {
 584                         tmpKey.text.getStyleClass().add("multi-char-text");
 585                     }
 586                     keys.add(tmpKey);
 587                 }
 588                 rows.add(keys);
 589             }
 590             currentBoard = rows;
 591             
 592             getChildren().clear();
 593             numCols = 0;
 594             for (List<Key> row : currentBoard) {
 595                 for (Key key : row) {
 596                     numCols = Math.max(numCols, key.col + key.colSpan);
 597                 }
 598                 getChildren().addAll(row);
 599             }
 600         }
 601     }
 602 
 603     /**
 604      * builds primary VK based on the keyboard
 605      * type set on the VirtualKeyboard.
 606      */
 607     private void rebuildPrimaryVK(String type) {
 608         currentBoard = loadBoard(type);
 609 
 610         //Clear all state keys and updates current board
 611         clearStateKeys();
 612         
 613         getChildren().clear();
 614         numCols = 0;
 615         for (List<Key> row : currentBoard) {
 616             for (Key key : row) {
 617                 numCols = Math.max(numCols, key.col + key.colSpan);
 618             }
 619             getChildren().addAll(row);
 620         }
 621     }
 622 
 623     // This skin is designed such that it gives equal widths to all columns. So
 624     // the pref width is just some hard-coded value (although I could have maybe
 625     // done it based on the pref width of a text node with the right font).
 626     @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) {
 627         return leftInset + (56 * numCols) + rightInset;
 628     }
 629 
 630     // Pref height is just some value. This isn't overly important.
 631     @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) {
 632         return topInset + (80 * 5) + bottomInset;
 633     }
 634 
 635     // Lays the buttons comprising the current keyboard out. 
 636     @Override
 637     protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
 638         // I have fixed width columns, all the same.
 639         int numRows = currentBoard.size();
 640         final double colWidth = ((contentWidth - ((numCols - 1) * GAP)) / numCols);
 641         double rowHeight = ((contentHeight - ((numRows - 1) * GAP)) / numRows);
 642         double rowY = contentY;
 643         for (List<Key> row : currentBoard) {
 644             for (Key key : row) {
 645                 double startX = contentX + (key.col * (colWidth + GAP));
 646                 double width = (key.colSpan * (colWidth + GAP)) - GAP;
 647                 key.resizeRelocate((int)(startX + .5), (int)(rowY + .5),
 648                                    width, rowHeight);
 649             }
 650             rowY += rowHeight + GAP;
 651         }
 652     }
 653 
 654 
 655     /**
 656      * A Key on the virtual keyboard. This is simply a Region. Some information
 657      * about the key relative to other keys on the layout is given by the col
 658      * and colSpan fields.
 659      */
 660     private class Key extends Region {
 661         int col = 0;
 662         int colSpan = 1;
 663         protected final Text text;
 664         protected final Text altText;
 665         protected final Region icon;
 666 
 667         protected Key() {
 668             icon = new Region();
 669             text = new Text();
 670             text.setTextOrigin(VPos.TOP);
 671             altText = new Text();
 672             altText.setTextOrigin(VPos.TOP);
 673             getChildren().setAll(text, altText, icon);
 674             getStyleClass().setAll("key");
 675             addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
 676                 if (event.getButton() == MouseButton.PRIMARY)
 677                     press();
 678             });
 679             addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
 680                 if (event.getButton() == MouseButton.PRIMARY)
 681                     release();
 682             });
 683         }
 684         protected void press() { }
 685         protected void release() {
 686             clearShift();
 687         }
 688 
 689         public void update(boolean capsDown, boolean shiftDown, boolean isSymbol) { }
 690 
 691         @Override protected void layoutChildren() {
 692             final double left = snappedLeftInset();
 693             final double top = snappedTopInset();
 694             final double width = getWidth() - left - snappedRightInset();
 695             final double height = getHeight() - top - snappedBottomInset();
 696 
 697             text.setVisible(icon.getBackground() == null);
 698             double contentPrefWidth = text.prefWidth(-1);
 699             double contentPrefHeight = text.prefHeight(-1);
 700             text.resizeRelocate(
 701                     (int) (left + ((width - contentPrefWidth) / 2) + .5),
 702                     (int) (top + ((height - contentPrefHeight) / 2) + .5),
 703                     (int) contentPrefWidth,
 704                     (int) contentPrefHeight);
 705 
 706             altText.setVisible(icon.getBackground() == null && altText.getText().length() > 0);
 707             contentPrefWidth = altText.prefWidth(-1);
 708             contentPrefHeight = altText.prefHeight(-1);
 709             altText.resizeRelocate(
 710                     (int) left + (width - contentPrefWidth) + .5,
 711                     (int) (top + ((height - contentPrefHeight) / 2) + .5 - height/2),
 712                     (int) contentPrefWidth,
 713                     (int) contentPrefHeight);
 714 
 715             icon.resizeRelocate(left-8, top-8, width+16, height+16);
 716         }
 717 
 718     }
 719 
 720     /**
 721      * Any key on the keyboard which will send a KeyEvent to the client. This
 722      * class just maintains the state and logic for firing an event, using the
 723      * "chars" and "code" as the values sent in the event. A subclass must set
 724      * these appropriately.
 725      */
 726     private class TextInputKey extends Key {
 727         String chars = "";
 728 
 729         protected void press() {
 730         }
 731         protected void release() {
 732             if (fxvk != secondaryVK && secondaryPopup != null && secondaryPopup.isShowing()) {
 733                 return;
 734             }
 735             sendKeyEvents();
 736             if (fxvk == secondaryVK) {
 737                 showSecondaryVK(null);
 738             }
 739             super.release();
 740         }
 741 
 742         protected void sendKeyEvents() {
 743             Node target = fxvk.getAttachedNode();
 744             if (target instanceof EventTarget) {
 745                 if (chars != null) {
 746                     target.fireEvent(new KeyEvent(KeyEvent.KEY_TYPED, chars, "", KeyCode.UNDEFINED, shiftDown, false, false, false));
 747                 }
 748             }
 749         }
 750     }
 751 
 752     /**
 753      * A key which has a letter, a number or symbol on it
 754      * 
 755      */
 756     private class CharKey extends TextInputKey {
 757         private final String letterChars;
 758         private final String altChars;
 759         private final String[] moreChars;
 760 
 761         private CharKey(String letter, String alt, String[] moreChars, String id) {
 762             this.letterChars = letter;
 763             this.altChars = alt;
 764             this.moreChars = moreChars;
 765             this.chars = this.letterChars;
 766 
 767             text.setText(this.chars);
 768             altText.setText(this.altChars);
 769             if (vkLookup) {
 770                 setId((id != null ? id : chars).replaceAll("\\.", ""));
 771             }
 772         }
 773 
 774         private CharKey(String letter, String alt, String[] moreChars) {
 775             this(letter, alt, moreChars, null);
 776         }
 777 
 778         protected void press() {
 779             super.press();
 780             if (letterChars.equals(altChars) && moreChars == null) {
 781                 return;
 782             }
 783             if (fxvk == primaryVK) {
 784                 showSecondaryVK(null);
 785                 secondaryVKKey = CharKey.this;
 786                 secondaryVKDelay.playFromStart();
 787             }
 788         }
 789 
 790         protected void release() {
 791             super.release();
 792             if (letterChars.equals(altChars) && moreChars == null) {
 793                 return;
 794             }
 795             if (fxvk == primaryVK) {
 796                 secondaryVKDelay.stop();
 797             }
 798         }
 799 
 800         @Override public void update(boolean capsDown, boolean shiftDown, boolean isSymbol) {
 801             if (isSymbol) {
 802                 chars = altChars;
 803                 text.setText(chars);
 804                 if (moreChars != null && moreChars.length > 0 && !Character.isLetter(moreChars[0].charAt(0))) {
 805                     altText.setText(moreChars[0]);
 806                 } else {
 807                     altText.setText(null);
 808                 }
 809             } else {
 810                 chars = (capsDown || shiftDown) ? letterChars.toUpperCase() : letterChars.toLowerCase();
 811                 text.setText(chars);
 812                 altText.setText(altChars);
 813             }
 814         }
 815     }
 816 
 817     /**
 818      * One of several TextInputKeys which have super powers, such as "Tab" and
 819      * "Return" and "Backspace". These keys still send events to the client,
 820      * but may also have additional state related functionality on the keyboard
 821      * such as the "Shift" key.
 822      */
 823     private class SuperKey extends TextInputKey {
 824         private SuperKey(String letter, String code) {
 825             this.chars = code;
 826             text.setText(letter);
 827             getStyleClass().add("special");
 828             if (vkLookup) {
 829                 setId(letter);
 830             }
 831         }
 832     }
 833 
 834     /**
 835      * Some keys actually do need to use KeyCode for pressed / released events,
 836      * and BackSpace is one of them.
 837      */
 838     private class KeyCodeKey extends SuperKey {
 839         private KeyCode code;
 840 
 841         private KeyCodeKey(String letter, String c, KeyCode code) {
 842             super(letter, c);
 843             this.code = code;
 844             if (vkLookup) {
 845                 setId(letter);
 846             }
 847         }
 848 
 849         protected void sendKeyEvents() {
 850             Node target = fxvk.getAttachedNode();
 851             if (target instanceof EventTarget) {               
 852                 target.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, KeyEvent.CHAR_UNDEFINED, chars, code, shiftDown, false, false, false));
 853                 target.fireEvent(new KeyEvent(KeyEvent.KEY_TYPED, chars, "", KeyCode.UNDEFINED, shiftDown, false, false, false));
 854                 target.fireEvent(new KeyEvent(KeyEvent.KEY_RELEASED, KeyEvent.CHAR_UNDEFINED, chars, code, shiftDown, false, false, false));
 855             }
 856         }
 857     }
 858 
 859     /**
 860      * These keys only manipulate the state of the keyboard and never
 861      * send key events to the client. For example, "Hide", "Caps Lock",
 862      * etc are all KeyboardStateKeys.
 863      */
 864     private class KeyboardStateKey extends Key {
 865         private final String defaultText;
 866         private final String toggledText;
 867 
 868         private KeyboardStateKey(String defaultText, String toggledText, String id) {
 869             this.defaultText = defaultText;
 870             this.toggledText = toggledText;
 871             text.setText(this.defaultText);
 872             if (vkLookup && id != null) {
 873                 setId(id);
 874             }
 875             getStyleClass().add("special");
 876         }
 877 
 878         @Override public void update(boolean capsDown, boolean shiftDown, boolean isSymbol) {
 879             //change icon
 880             
 881             if (isSymbol) {
 882                 text.setText(this.toggledText);
 883             } else {
 884                 text.setText(this.defaultText);
 885             }
 886         }
 887     }
 888 
 889     private void showSecondaryVK(final CharKey key) {
 890         if (key != null) {
 891             final Node textInput = primaryVK.getAttachedNode();
 892 
 893             if (secondaryVK == null) {
 894                 secondaryVK = new FXVK();
 895                 //secondaryVK.getStyleClass().addAll("fxvk-secondary", "fxvk-portrait");
 896                 secondaryVK.setSkin(new FXVKSkin(secondaryVK));
 897                 secondaryVK.getStyleClass().setAll("fxvk-secondary");
 898                 secondaryPopup = new Popup();
 899                 secondaryPopup.setAutoHide(true);
 900                 secondaryPopup.getContent().add(secondaryVK);
 901             }
 902            
 903             secondaryVK.chars=null;
 904             ArrayList<String> secondaryList = new ArrayList<String>();
 905 
 906             // Add primary character
 907             if (!isSymbol) {
 908                 if (key.letterChars != null && key.letterChars.length() > 0) {
 909                     if (shiftDown || capsDown) {
 910                         secondaryList.add(key.letterChars.toUpperCase());
 911                     } else {
 912                         secondaryList.add(key.letterChars);
 913                     }
 914                 }
 915             }
 916 
 917             // Add secondary character
 918             if (key.altChars != null && key.altChars.length() > 0) {
 919                 if (shiftDown || capsDown) {
 920                     secondaryList.add(key.altChars.toUpperCase());
 921                 } else {
 922                     secondaryList.add(key.altChars);
 923                 }
 924             }
 925             
 926             // Add more letters
 927             if (key.moreChars != null && key.moreChars.length > 0) {
 928                 if (isSymbol) {
 929                     //Add non-letters
 930                     for (String ch : key.moreChars) {
 931                         if (!Character.isLetter(ch.charAt(0))) {
 932                             secondaryList.add(ch);
 933                         }
 934                     }
 935                  } else {
 936                     //Add letters
 937                     for (String ch : key.moreChars) {
 938                         if (Character.isLetter(ch.charAt(0))) {
 939                             if (shiftDown || capsDown) {
 940                                 secondaryList.add(ch.toUpperCase());
 941                             } else {
 942                                 secondaryList.add(ch);
 943                             }
 944                         }
 945                     }
 946                 }
 947             }
 948             
 949             boolean isMultiChar = false;
 950             for (String s : secondaryList) {
 951                 if (s.length() > 1 ) {
 952                     isMultiChar = true;
 953                 }
 954             }
 955             
 956             secondaryVK.chars = secondaryList.toArray(new String[secondaryList.size()]);
 957 
 958             if (secondaryVK.chars.length > 1) {
 959                 if (secondaryVK.getSkin() != null) {
 960                     ((FXVKSkin)secondaryVK.getSkin()).rebuildSecondaryVK();
 961                 }
 962 
 963                 secondaryVK.setAttachedNode(textInput);
 964                 FXVKSkin primarySkin = (FXVKSkin)primaryVK.getSkin();
 965                 FXVKSkin secondarySkin = (FXVKSkin)secondaryVK.getSkin();
 966                 //Insets insets = secondarySkin.getInsets();
 967                 int nKeys = secondaryVK.chars.length;
 968                 int nRows = (int)Math.floor(Math.sqrt(Math.max(1, nKeys - 2)));
 969                 int nKeysPerRow = (int)Math.ceil(nKeys / (double)nRows);
 970                 
 971                 final double w = snappedLeftInset() + snappedRightInset() +
 972                                  nKeysPerRow * PREF_PORTRAIT_KEY_WIDTH * (isMultiChar ? 2 : 1) + (nKeysPerRow - 1) * GAP;
 973                 final double h = snappedTopInset() + snappedBottomInset() +
 974                                  nRows * PREF_KEY_HEIGHT + (nRows-1) * GAP;
 975 
 976                 secondaryVK.setPrefWidth(w);
 977                 secondaryVK.setMinWidth(USE_PREF_SIZE);
 978                 secondaryVK.setPrefHeight(h);
 979                 secondaryVK.setMinHeight(USE_PREF_SIZE);
 980                 Platform.runLater(() -> {
 981                     // Position popup on screen
 982                     Point2D nodePoint =
 983                         com.sun.javafx.util.Utils.pointRelativeTo(key, w, h, HPos.CENTER, VPos.TOP,
 984                                                              5, -3, true);
 985                     double x = nodePoint.getX();
 986                     double y = nodePoint.getY();
 987                     Scene scene = key.getScene();
 988                     x = Math.min(x, scene.getWindow().getX() + scene.getWidth() - w);
 989                     secondaryPopup.show(key.getScene().getWindow(), x, y);
 990                 });
 991             }
 992         } else {
 993             closeSecondaryVK();
 994         }
 995     }
 996 
 997 
 998     private List<List<Key>> loadBoard(String type) {
 999         List<List<Key>> tmpBoard = boardMap.get(type);
1000         if (tmpBoard != null) {
1001             return tmpBoard;
1002         }
1003 
1004         String boardFileName = type.substring(0,1).toUpperCase() + type.substring(1).toLowerCase() + "Board.txt";
1005         try {
1006             tmpBoard = new ArrayList<List<Key>>(5);
1007             List<Key> keys = new ArrayList<Key>(20);
1008 
1009             InputStream boardFile = FXVKSkin.class.getResourceAsStream(boardFileName);
1010             BufferedReader reader = new BufferedReader(new InputStreamReader(boardFile));
1011             String line;
1012             // A pointer to the current column. This will be incremented for every string
1013             // of text, or space.
1014             int c = 0;
1015             // The col at which the key will be placed
1016             int col = 0;
1017             // The number of columns that the key will span
1018             int colSpan = 1;
1019             // Whether the "chars" is an identifier, like $shift or $SymbolBoard, etc.
1020             boolean identifier = false;
1021             // The textual content of the Key
1022             List<String> charsList = new ArrayList<String>(10);
1023 
1024             while ((line = reader.readLine()) != null) {
1025                 if (line.length() == 0 || line.charAt(0) == '#') {
1026                     continue;
1027                 }
1028                 // A single line represents a single row of buttons
1029                 for (int i=0; i<line.length(); i++) {
1030                     char ch = line.charAt(i);
1031 
1032                     // Process the char
1033                     if (ch == ' ') {
1034                         c++;
1035                     } else if (ch == '[') {
1036                         // Start of a key
1037                         col = c;
1038                         charsList = new ArrayList<String>(10);
1039                         identifier = false;
1040                     } else if (ch == ']') {
1041                         String chars = "";
1042                         String alt = null;
1043                         String[] moreChars = null;
1044 
1045                         for (int idx = 0; idx < charsList.size(); idx++) {
1046                             charsList.set(idx, FXVKCharEntities.get(charsList.get(idx)));
1047                         }
1048                 
1049                         int listSize = charsList.size();
1050                         if (listSize > 0) {
1051                             chars = charsList.get(0);
1052                             if (listSize > 1) {
1053                                 alt = charsList.get(1);
1054                                 if (listSize > 2) {
1055                                     moreChars = charsList.subList(2, listSize).toArray(new String[listSize - 2]);
1056                                 }
1057                             }
1058                         }
1059                         
1060                         // End of a key
1061                         colSpan = c - col;
1062                         Key key;
1063                         if (identifier) {
1064                             if ("$shift".equals(chars)) {
1065                                 key = new KeyboardStateKey("", null, "shift") {
1066                                     @Override protected void release() {
1067                                         pressShift();
1068                                     }
1069                                     
1070                                     @Override public void update(boolean capsDown, boolean shiftDown, boolean isSymbol) {
1071                                         if (isSymbol) {
1072                                             this.setDisable(true);
1073                                             this.setVisible(false);
1074                                         } else {
1075                                             if (capsDown) {
1076                                                 icon.getStyleClass().remove("shift-icon");
1077                                                 icon.getStyleClass().add("capslock-icon");
1078                                             } else {
1079                                                 icon.getStyleClass().remove("capslock-icon");
1080                                                 icon.getStyleClass().add("shift-icon");
1081                                             }
1082                                             this.setDisable(false);
1083                                             this.setVisible(true);
1084                                         }
1085                                     }
1086                                 };
1087                                 key.getStyleClass().add("shift");
1088 
1089                             } else if ("$SymbolABC".equals(chars)) {
1090                                 key = new KeyboardStateKey("!#123", "ABC", "symbol") {
1091                                     @Override protected void release() {
1092                                         pressSymbolABC();
1093                                     }
1094                                 };
1095                             } else if ("$backspace".equals(chars)) {
1096                                 key = new KeyCodeKey("backspace", "\b", KeyCode.BACK_SPACE) {
1097                                     @Override protected void press() {
1098                                         if (KEY_REPEAT_RATE > 0) {
1099                                             clearShift();
1100                                             sendKeyEvents();
1101                                             repeatKey = this;
1102                                             repeatInitialDelay.playFromStart();
1103                                         } else {
1104                                             super.press();
1105                                         }
1106                                     }
1107                                     @Override protected void release() {
1108                                         if (KEY_REPEAT_RATE > 0) {
1109                                             repeatInitialDelay.stop();
1110                                             repeatSubsequentDelay.stop();
1111                                         } else {
1112                                             super.release();
1113                                         }
1114                                     }
1115                                 };
1116                                 key.getStyleClass().add("backspace");
1117                             } else if ("$enter".equals(chars)) {
1118                                 key = new KeyCodeKey("enter", "\n", KeyCode.ENTER);
1119                                 key.getStyleClass().add("enter");
1120                             } else if ("$tab".equals(chars)) {
1121                                 key = new KeyCodeKey("tab", "\t", KeyCode.TAB);
1122                             } else if ("$space".equals(chars)) {
1123                                 key = new CharKey(" ", " ", null, "space");
1124                             } else if ("$clear".equals(chars)) {
1125                                 key = new SuperKey("clear", "");
1126                             } else if ("$.org".equals(chars)) {
1127                                 key = new SuperKey(".org", ".org");
1128                             } else if ("$.com".equals(chars)) {
1129                                 key = new SuperKey(".com", ".com");
1130                             } else if ("$.net".equals(chars)) {
1131                                 key = new SuperKey(".net", ".net");
1132                             } else if ("$oracle.com".equals(chars)) {
1133                                 key = new SuperKey("oracle.com", "oracle.com");
1134                             } else if ("$gmail.com".equals(chars)) {
1135                                 key = new SuperKey("gmail.com", "gmail.com");
1136                             } else if ("$hide".equals(chars)) {
1137                                 key = new KeyboardStateKey("hide", null, "hide") {
1138                                     @Override protected void release() {
1139                                         isVKHidden = true;
1140                                         startSlideOut(false);
1141                                         // Restore window position
1142                                         if (vkAdjustWindow) {
1143                                             restoreWindowPosition(attachedNode);
1144                                         }
1145                                     }
1146                                 };
1147                                 key.getStyleClass().add("hide");
1148                             } else if ("$undo".equals(chars)) {
1149                                 key = new SuperKey("undo", "");
1150                             } else if ("$redo".equals(chars)) {
1151                                 key = new SuperKey("redo", "");
1152                             } else {
1153                                 //Unknown Key
1154                                 key = null;
1155                             }
1156                         } else {
1157                             key = new CharKey(chars, alt, moreChars);
1158                         }
1159                         if (key != null) {
1160                             key.col = col;
1161                             key.colSpan = colSpan;
1162                             for (String sc : key.getStyleClass()) {
1163                                 key.text.getStyleClass().add(sc + "-text");
1164                                 key.altText.getStyleClass().add(sc + "-alttext");
1165                                 key.icon.getStyleClass().add(sc + "-icon");
1166                             }
1167                             if (chars != null && chars.length() > 1) {
1168                                 key.text.getStyleClass().add("multi-char-text");
1169                             }
1170                             if (alt != null && alt.length() > 1) {
1171                                 key.altText.getStyleClass().add("multi-char-text");
1172                             }
1173 
1174                             keys.add(key);
1175                         }
1176                     } else {
1177                         // Normal textual characters. Read all the way up to the
1178                         // next ] or space
1179                         for (int j=i; j<line.length(); j++) {
1180                             char c2 = line.charAt(j);
1181                             boolean e = false;
1182                             if (c2 == '\\') {
1183                                 j++;
1184                                 i++;
1185                                 e = true;
1186                                 c2 = line.charAt(j);
1187                             }
1188 
1189                             if (c2 == '$' && !e) {
1190                                 identifier = true;
1191                             }
1192 
1193                             if (c2 == '|' && !e) {
1194                                 charsList.add(line.substring(i, j));
1195                                 i = j + 1;
1196                             } else if ((c2 == ']' || c2 == ' ') && !e) {
1197                                 charsList.add(line.substring(i, j));
1198                                 i = j-1;
1199                                 break;
1200                             }
1201                         }
1202                         c++;
1203                     }
1204                 }
1205 
1206                 c = 0;
1207                 col = 0;
1208                 tmpBoard.add(keys);
1209                 keys = new ArrayList<Key>(20);
1210             }
1211             reader.close();
1212             boardMap.put(type, tmpBoard);
1213             return tmpBoard;
1214         } catch (Exception e) {
1215             e.printStackTrace();
1216             return Collections.emptyList();
1217         }
1218     }
1219 }