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 }