/* * Copyright (c) 2010, 2015, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package com.sun.javafx.scene.control.skin; import javafx.animation.Animation; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.InvalidationListener; import javafx.beans.Observable; import javafx.beans.property.DoubleProperty; import javafx.beans.property.SimpleDoubleProperty; import javafx.event.EventHandler; import javafx.event.EventTarget; import javafx.geometry.Bounds; import javafx.geometry.HPos; import javafx.geometry.Point2D; import javafx.geometry.Rectangle2D; import javafx.geometry.VPos; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.control.TextInputControl; import javafx.scene.control.TextField; import javafx.scene.control.TextArea; import javafx.scene.control.ComboBoxBase; import javafx.scene.Scene; import javafx.scene.input.InputEvent; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.input.MouseEvent; import javafx.scene.input.MouseButton; import javafx.scene.layout.Region; import javafx.scene.text.Text; import javafx.stage.Popup; import javafx.stage.Window; import javafx.util.Duration; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; import java.util.HashMap; import com.sun.javafx.scene.control.behavior.BehaviorBase; import static javafx.scene.input.MouseEvent.MOUSE_PRESSED; import static javafx.scene.input.TouchEvent.TOUCH_PRESSED; import static javafx.scene.layout.Region.USE_PREF_SIZE; import java.security.AccessController; import java.security.PrivilegedAction; public class FXVKSkin extends BehaviorSkinBase> { private static final int GAP = 6; private List> currentBoard; private static HashMap>> boardMap = new HashMap>>(); private int numCols; private boolean capsDown = false; private boolean shiftDown = false; private boolean isSymbol = false; long lastTime = -1L; void clearShift() { if (shiftDown && !capsDown) { shiftDown = false; updateKeys(); } lastTime = -1L; } void pressShift() { long time = System.currentTimeMillis(); //potential for a shift lock if (shiftDown && !capsDown) { if (lastTime > 0L && time - lastTime < 400L) { //set caps lock shiftDown = false; capsDown = true; } else { //set normal shiftDown = false; capsDown = false; } } else if (!shiftDown && !capsDown) { // set shift shiftDown=true; } else { //set to normal shiftDown = false; capsDown = false; } updateKeys(); lastTime = time; } void clearSymbolABC() { isSymbol = false; updateKeys(); } void pressSymbolABC() { isSymbol = !isSymbol; updateKeys(); } void clearStateKeys() { capsDown = false; shiftDown = false; isSymbol = false; lastTime = -1L; updateKeys(); } private void updateKeys() { for (List row : currentBoard) { for (Key key : row) { key.update(capsDown, shiftDown, isSymbol); } } } private static Popup vkPopup; private static Popup secondaryPopup; private static FXVK primaryVK; private static Timeline slideInTimeline = new Timeline(); private static Timeline slideOutTimeline = new Timeline(); private static boolean hideAfterSlideOut = false; private static FXVK secondaryVK; private static Timeline secondaryVKDelay; private static CharKey secondaryVKKey; private static TextInputKey repeatKey; private static Timeline repeatInitialDelay; private static Timeline repeatSubsequentDelay; // key repeat initial delay (ms) private static double KEY_REPEAT_DELAY = 400; private static double KEY_REPEAT_DELAY_MIN = 100; private static double KEY_REPEAT_DELAY_MAX = 1000; // key repeat rate (cps) private static double KEY_REPEAT_RATE = 25; private static double KEY_REPEAT_RATE_MIN = 2; private static double KEY_REPEAT_RATE_MAX = 50; private Node attachedNode; private String vkType = null; FXVK fxvk; static final double VK_HEIGHT = 243; static final double VK_SLIDE_MILLIS = 250; static final double PREF_PORTRAIT_KEY_WIDTH = 40; static final double PREF_KEY_HEIGHT = 56; static boolean vkAdjustWindow = false; static boolean vkLookup = false; static { AccessController.doPrivileged((PrivilegedAction) () -> { String s = System.getProperty("com.sun.javafx.vk.adjustwindow"); if (s != null) { vkAdjustWindow = Boolean.valueOf(s); } s = System.getProperty("com.sun.javafx.sqe.vk.lookup"); if (s != null) { vkLookup = Boolean.valueOf(s); } s = System.getProperty("com.sun.javafx.virtualKeyboard.backspaceRepeatDelay"); if (s != null) { Double delay = Double.valueOf(s); KEY_REPEAT_DELAY = Math.min(Math.max(delay, KEY_REPEAT_DELAY_MIN), KEY_REPEAT_DELAY_MAX); } s = System.getProperty("com.sun.javafx.virtualKeyboard.backspaceRepeatRate"); if (s != null) { Double rate = Double.valueOf(s); if (rate <= 0) { //disable key repeat KEY_REPEAT_RATE = 0; } else { KEY_REPEAT_RATE = Math.min(Math.max(rate, KEY_REPEAT_RATE_MIN), KEY_REPEAT_RATE_MAX); } } return null; }); } // Proxy for read-only Window.yProperty() so we can animate. private static DoubleProperty winY = new SimpleDoubleProperty(); static { winY.addListener(valueModel -> { if (vkPopup != null) { vkPopup.setY(winY.get()); } }); } private static void startSlideIn() { slideOutTimeline.stop(); slideInTimeline.playFromStart(); } private static void startSlideOut(boolean doHide) { hideAfterSlideOut = doHide; slideInTimeline.stop(); slideOutTimeline.playFromStart(); } private void adjustWindowPosition(final Node node) { if ( !(node instanceof TextInputControl) ) { return; } // attached node y position in window coordinates double inputControlMinY = node.localToScene(0.0, 0.0).getY() + node.getScene().getY(); double inputControlHeight = ((TextInputControl) node).getHeight(); double inputControlMaxY = inputControlMinY + inputControlHeight; double screenHeight = com.sun.javafx.util.Utils.getScreen(node).getBounds().getHeight(); double visibleAreaMaxY = screenHeight - VK_HEIGHT; double inputLineCenterY = 0.0; double inputLineBottomY = 0.0; double newWindowYPos = 0.0; double screenTopOffset = 10.0; if (node instanceof TextField) { inputLineCenterY = inputControlMinY + inputControlHeight / 2; inputLineBottomY = inputControlMaxY; //check for combo box Parent parent = attachedNode.getParent(); if (parent instanceof ComboBoxBase) { //combo box // position near screen top newWindowYPos = Math.min(screenTopOffset - inputControlMinY, 0); } else { // position at center of visible screen area newWindowYPos = Math.min(visibleAreaMaxY / 2 - inputLineCenterY, 0); } } else if (node instanceof TextArea) { TextAreaSkin textAreaSkin = (TextAreaSkin)((TextArea)node).getSkin(); Bounds caretBounds = textAreaSkin.getCaretBounds(); double caretMinY = caretBounds.getMinY(); double caretMaxY = caretBounds.getMaxY(); inputLineCenterY = inputControlMinY + ( caretMinY + caretMaxY ) / 2; inputLineBottomY = inputControlMinY + caretMaxY; if (inputControlHeight < visibleAreaMaxY) { // position at center of visible screen area newWindowYPos = visibleAreaMaxY / 2 - (inputControlMinY + inputControlHeight / 2); } else { // position the line containing the caret at center of visible screen area newWindowYPos = visibleAreaMaxY / 2 - inputLineCenterY; } newWindowYPos = Math.min(newWindowYPos, 0); } else { inputLineCenterY = inputControlMinY + inputControlHeight / 2; inputLineBottomY = inputControlMaxY; // position at center of visible screen area newWindowYPos = Math.min(visibleAreaMaxY / 2 - inputLineCenterY, 0); } Window w = node.getScene().getWindow(); if (origWindowYPos + inputLineBottomY > visibleAreaMaxY) { w.setY(newWindowYPos); } else { w.setY(origWindowYPos); } } private void saveWindowPosition(final Node node) { Window w = node.getScene().getWindow(); origWindowYPos = w.getY(); } private void restoreWindowPosition(final Node node) { if (node != null) { Scene scene = node.getScene(); if (scene != null) { Window window = scene.getWindow(); if (window != null) { window.setY(origWindowYPos); } } } } EventHandler unHideEventHandler; private boolean isVKHidden = false; private Double origWindowYPos = null; private void registerUnhideHandler(final Node node) { if (unHideEventHandler == null) { unHideEventHandler = event -> { if (attachedNode != null && isVKHidden) { double screenHeight = com.sun.javafx.util.Utils.getScreen(attachedNode).getBounds().getHeight(); if (fxvk.getHeight() > 0 && (vkPopup.getY() > screenHeight - fxvk.getHeight())) { if (slideInTimeline.getStatus() != Animation.Status.RUNNING) { startSlideIn(); if (vkAdjustWindow) { adjustWindowPosition(attachedNode); } } } } isVKHidden = false; }; } node.addEventHandler(TOUCH_PRESSED, unHideEventHandler); node.addEventHandler(MOUSE_PRESSED, unHideEventHandler); } private void unRegisterUnhideHandler(Node node) { if (unHideEventHandler != null) { node.removeEventHandler(TOUCH_PRESSED, unHideEventHandler); node.removeEventHandler(MOUSE_PRESSED, unHideEventHandler); } } private String getNodeVKType(Node node) { Object typeValue = node.getProperties().get(FXVK.VK_TYPE_PROP_KEY); String typeStr = null; if (typeValue instanceof String) { typeStr = ((String)typeValue).toLowerCase(Locale.ROOT); } return (typeStr != null ? typeStr : "text"); } private void updateKeyboardType(Node node) { String oldType = vkType; vkType = getNodeVKType(node); //VK type changed, rebuild if ( oldType == null || !vkType.equals(oldType) ) { rebuildPrimaryVK(vkType); } } private void closeSecondaryVK() { if (secondaryVK != null) { secondaryVK.setAttachedNode(null); secondaryPopup.hide(); } } private void setupPrimaryVK() { fxvk.setFocusTraversable(false); fxvk.setVisible(true); // init popup window and slide animations if (vkPopup == null) { vkPopup = new Popup(); vkPopup.setAutoFix(false); } vkPopup.getContent().setAll(fxvk); double screenHeight = com.sun.javafx.util.Utils.getScreen(fxvk).getBounds().getHeight(); double width = com.sun.javafx.util.Utils.getScreen(fxvk).getBounds().getWidth(); //Setup VK slide animations slideInTimeline.getKeyFrames().setAll( new KeyFrame(Duration.millis(VK_SLIDE_MILLIS), new KeyValue(winY, screenHeight - VK_HEIGHT, Interpolator.EASE_BOTH))); slideOutTimeline.getKeyFrames().setAll( new KeyFrame(Duration.millis(VK_SLIDE_MILLIS), event -> { if (hideAfterSlideOut && vkPopup.isShowing()) { vkPopup.hide(); } }, new KeyValue(winY, screenHeight, Interpolator.EASE_BOTH))); //Set VK size fxvk.setPrefWidth(width); fxvk.setMinWidth(USE_PREF_SIZE); fxvk.setMaxWidth(USE_PREF_SIZE); fxvk.setPrefHeight(VK_HEIGHT); fxvk.setMinHeight(USE_PREF_SIZE); //set up long-press triger for secondary VK if (secondaryVKDelay == null) { secondaryVKDelay = new Timeline(); } KeyFrame kf = new KeyFrame(Duration.millis(500), event -> { if (secondaryVKKey != null) { showSecondaryVK(secondaryVKKey); } }); secondaryVKDelay.getKeyFrames().setAll(kf); //Setup key repeat animations if (KEY_REPEAT_RATE > 0) { repeatInitialDelay = new Timeline(new KeyFrame( Duration.millis(KEY_REPEAT_DELAY), event -> { //fire current key repeatKey.sendKeyEvents(); //Start repeat animation repeatSubsequentDelay.playFromStart(); } )); repeatSubsequentDelay = new Timeline(new KeyFrame( Duration.millis(1000.0 / KEY_REPEAT_RATE), event -> { //fire current key repeatKey.sendKeyEvents(); } )); repeatSubsequentDelay.setCycleCount(Animation.INDEFINITE); } } void prerender(Node node) { if (fxvk != primaryVK) { return; } //Preload all boards loadBoard("text"); loadBoard("numeric"); loadBoard("url"); loadBoard("email"); updateKeyboardType(node); fxvk.setVisible(true); if (!vkPopup.isShowing()) { Rectangle2D screenBounds = com.sun.javafx.util.Utils.getScreen(node).getBounds(); vkPopup.setX((screenBounds.getWidth() - fxvk.prefWidth(-1)) / 2); winY.set(screenBounds.getHeight()); vkPopup.show(node.getScene().getWindow()); } } public FXVKSkin(final FXVK fxvk) { super(fxvk, new BehaviorBase<>(fxvk, Collections.emptyList())); this.fxvk = fxvk; if (fxvk == FXVK.vk) { primaryVK = fxvk; } if (fxvk == primaryVK) { setupPrimaryVK(); } fxvk.attachedNodeProperty().addListener(new InvalidationListener() { @Override public void invalidated(Observable valueModel) { Node oldNode = attachedNode; attachedNode = fxvk.getAttachedNode(); if (fxvk != primaryVK) { return; } closeSecondaryVK(); if (attachedNode != null) { if (oldNode != null) { unRegisterUnhideHandler(oldNode); } registerUnhideHandler(attachedNode); updateKeyboardType(attachedNode); //owner window has changed so hide VK and show with new owner if (oldNode == null || oldNode.getScene() == null || oldNode.getScene().getWindow() != attachedNode.getScene().getWindow()) { if (vkPopup.isShowing()) { vkPopup.hide(); } else { } } if (!vkPopup.isShowing()) { Rectangle2D screenBounds = com.sun.javafx.util.Utils.getScreen(attachedNode).getBounds(); vkPopup.setX((screenBounds.getWidth() - fxvk.prefWidth(-1)) / 2); if (oldNode == null || isVKHidden) { //position off screen winY.set(screenBounds.getHeight()); } else { //position on screen (no slide in) winY.set(screenBounds.getHeight() - VK_HEIGHT); } vkPopup.show(attachedNode.getScene().getWindow()); } if (oldNode == null || isVKHidden) { startSlideIn(); } if (vkAdjustWindow) { //update previous window position only if moving from non-input control node or window has changed. if (oldNode == null || oldNode.getScene() == null || oldNode.getScene().getWindow() != attachedNode.getScene().getWindow()) { saveWindowPosition(attachedNode); } // Move window containing input node adjustWindowPosition(attachedNode); } } else { // attachedNode == null if (oldNode != null) { unRegisterUnhideHandler(oldNode); } startSlideOut(true); // Restore window position if (vkAdjustWindow) { restoreWindowPosition(oldNode); } } isVKHidden = false; } }); } /** * builds secondary (long-press) VK */ private void rebuildSecondaryVK() { if (secondaryVK.chars == null) { } else { int nKeys = secondaryVK.chars.length; int nRows = (int)Math.floor(Math.sqrt(Math.max(1, nKeys - 2))); int nKeysPerRow = (int)Math.ceil(nKeys / (double)nRows); Key tmpKey; List> rows = new ArrayList>(2); for (int i = 0; i < nRows; i++) { int start = i * nKeysPerRow; int end = Math.min(start + nKeysPerRow, nKeys); if (start >= end) break; List keys = new ArrayList(nKeysPerRow); for (int j = start; j < end; j++) { tmpKey = new CharKey(secondaryVK.chars[j], null, null); tmpKey.col= (j - start) * 2; tmpKey.colSpan = 2; for (String sc : tmpKey.getStyleClass()) { tmpKey.text.getStyleClass().add(sc + "-text"); tmpKey.altText.getStyleClass().add(sc + "-alttext"); tmpKey.icon.getStyleClass().add(sc + "-icon"); } if (secondaryVK.chars[j] != null && secondaryVK.chars[j].length() > 1) { tmpKey.text.getStyleClass().add("multi-char-text"); } keys.add(tmpKey); } rows.add(keys); } currentBoard = rows; getChildren().clear(); numCols = 0; for (List row : currentBoard) { for (Key key : row) { numCols = Math.max(numCols, key.col + key.colSpan); } getChildren().addAll(row); } } } /** * builds primary VK based on the keyboard * type set on the VirtualKeyboard. */ private void rebuildPrimaryVK(String type) { currentBoard = loadBoard(type); //Clear all state keys and updates current board clearStateKeys(); getChildren().clear(); numCols = 0; for (List row : currentBoard) { for (Key key : row) { numCols = Math.max(numCols, key.col + key.colSpan); } getChildren().addAll(row); } } // This skin is designed such that it gives equal widths to all columns. So // the pref width is just some hard-coded value (although I could have maybe // done it based on the pref width of a text node with the right font). @Override protected double computePrefWidth(double height, double topInset, double rightInset, double bottomInset, double leftInset) { return leftInset + (56 * numCols) + rightInset; } // Pref height is just some value. This isn't overly important. @Override protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset, double leftInset) { return topInset + (80 * 5) + bottomInset; } // Lays the buttons comprising the current keyboard out. @Override protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) { // I have fixed width columns, all the same. int numRows = currentBoard.size(); final double colWidth = ((contentWidth - ((numCols - 1) * GAP)) / numCols); double rowHeight = ((contentHeight - ((numRows - 1) * GAP)) / numRows); double rowY = contentY; for (List row : currentBoard) { for (Key key : row) { double startX = contentX + (key.col * (colWidth + GAP)); double width = (key.colSpan * (colWidth + GAP)) - GAP; key.resizeRelocate((int)(startX + .5), (int)(rowY + .5), width, rowHeight); } rowY += rowHeight + GAP; } } /** * A Key on the virtual keyboard. This is simply a Region. Some information * about the key relative to other keys on the layout is given by the col * and colSpan fields. */ private class Key extends Region { int col = 0; int colSpan = 1; protected final Text text; protected final Text altText; protected final Region icon; protected Key() { icon = new Region(); text = new Text(); text.setTextOrigin(VPos.TOP); altText = new Text(); altText.setTextOrigin(VPos.TOP); getChildren().setAll(text, altText, icon); getStyleClass().setAll("key"); addEventHandler(MouseEvent.MOUSE_PRESSED, event -> { if (event.getButton() == MouseButton.PRIMARY) press(); }); addEventHandler(MouseEvent.MOUSE_RELEASED, event -> { if (event.getButton() == MouseButton.PRIMARY) release(); }); } protected void press() { } protected void release() { clearShift(); } public void update(boolean capsDown, boolean shiftDown, boolean isSymbol) { } @Override protected void layoutChildren() { final double left = snappedLeftInset(); final double top = snappedTopInset(); final double width = getWidth() - left - snappedRightInset(); final double height = getHeight() - top - snappedBottomInset(); text.setVisible(icon.getBackground() == null); double contentPrefWidth = text.prefWidth(-1); double contentPrefHeight = text.prefHeight(-1); text.resizeRelocate( (int) (left + ((width - contentPrefWidth) / 2) + .5), (int) (top + ((height - contentPrefHeight) / 2) + .5), (int) contentPrefWidth, (int) contentPrefHeight); altText.setVisible(icon.getBackground() == null && altText.getText().length() > 0); contentPrefWidth = altText.prefWidth(-1); contentPrefHeight = altText.prefHeight(-1); altText.resizeRelocate( (int) left + (width - contentPrefWidth) + .5, (int) (top + ((height - contentPrefHeight) / 2) + .5 - height/2), (int) contentPrefWidth, (int) contentPrefHeight); icon.resizeRelocate(left-8, top-8, width+16, height+16); } } /** * Any key on the keyboard which will send a KeyEvent to the client. This * class just maintains the state and logic for firing an event, using the * "chars" and "code" as the values sent in the event. A subclass must set * these appropriately. */ private class TextInputKey extends Key { String chars = ""; protected void press() { } protected void release() { if (fxvk != secondaryVK && secondaryPopup != null && secondaryPopup.isShowing()) { return; } sendKeyEvents(); if (fxvk == secondaryVK) { showSecondaryVK(null); } super.release(); } protected void sendKeyEvents() { Node target = fxvk.getAttachedNode(); if (target instanceof EventTarget) { if (chars != null) { target.fireEvent(new KeyEvent(KeyEvent.KEY_TYPED, chars, "", KeyCode.UNDEFINED, shiftDown, false, false, false)); } } } } /** * A key which has a letter, a number or symbol on it * */ private class CharKey extends TextInputKey { private final String letterChars; private final String altChars; private final String[] moreChars; private CharKey(String letter, String alt, String[] moreChars, String id) { this.letterChars = letter; this.altChars = alt; this.moreChars = moreChars; this.chars = this.letterChars; text.setText(this.chars); altText.setText(this.altChars); if (vkLookup) { setId((id != null ? id : chars).replaceAll("\\.", "")); } } private CharKey(String letter, String alt, String[] moreChars) { this(letter, alt, moreChars, null); } protected void press() { super.press(); if (letterChars.equals(altChars) && moreChars == null) { return; } if (fxvk == primaryVK) { showSecondaryVK(null); secondaryVKKey = CharKey.this; secondaryVKDelay.playFromStart(); } } protected void release() { super.release(); if (letterChars.equals(altChars) && moreChars == null) { return; } if (fxvk == primaryVK) { secondaryVKDelay.stop(); } } @Override public void update(boolean capsDown, boolean shiftDown, boolean isSymbol) { if (isSymbol) { chars = altChars; text.setText(chars); if (moreChars != null && moreChars.length > 0 && !Character.isLetter(moreChars[0].charAt(0))) { altText.setText(moreChars[0]); } else { altText.setText(null); } } else { chars = (capsDown || shiftDown) ? letterChars.toUpperCase() : letterChars.toLowerCase(); text.setText(chars); altText.setText(altChars); } } } /** * One of several TextInputKeys which have super powers, such as "Tab" and * "Return" and "Backspace". These keys still send events to the client, * but may also have additional state related functionality on the keyboard * such as the "Shift" key. */ private class SuperKey extends TextInputKey { private SuperKey(String letter, String code) { this.chars = code; text.setText(letter); getStyleClass().add("special"); if (vkLookup) { setId(letter); } } } /** * Some keys actually do need to use KeyCode for pressed / released events, * and BackSpace is one of them. */ private class KeyCodeKey extends SuperKey { private KeyCode code; private KeyCodeKey(String letter, String c, KeyCode code) { super(letter, c); this.code = code; if (vkLookup) { setId(letter); } } protected void sendKeyEvents() { Node target = fxvk.getAttachedNode(); if (target instanceof EventTarget) { target.fireEvent(new KeyEvent(KeyEvent.KEY_PRESSED, KeyEvent.CHAR_UNDEFINED, chars, code, shiftDown, false, false, false)); target.fireEvent(new KeyEvent(KeyEvent.KEY_TYPED, chars, "", KeyCode.UNDEFINED, shiftDown, false, false, false)); target.fireEvent(new KeyEvent(KeyEvent.KEY_RELEASED, KeyEvent.CHAR_UNDEFINED, chars, code, shiftDown, false, false, false)); } } } /** * These keys only manipulate the state of the keyboard and never * send key events to the client. For example, "Hide", "Caps Lock", * etc are all KeyboardStateKeys. */ private class KeyboardStateKey extends Key { private final String defaultText; private final String toggledText; private KeyboardStateKey(String defaultText, String toggledText, String id) { this.defaultText = defaultText; this.toggledText = toggledText; text.setText(this.defaultText); if (vkLookup && id != null) { setId(id); } getStyleClass().add("special"); } @Override public void update(boolean capsDown, boolean shiftDown, boolean isSymbol) { //change icon if (isSymbol) { text.setText(this.toggledText); } else { text.setText(this.defaultText); } } } private void showSecondaryVK(final CharKey key) { if (key != null) { final Node textInput = primaryVK.getAttachedNode(); if (secondaryVK == null) { secondaryVK = new FXVK(); //secondaryVK.getStyleClass().addAll("fxvk-secondary", "fxvk-portrait"); secondaryVK.setSkin(new FXVKSkin(secondaryVK)); secondaryVK.getStyleClass().setAll("fxvk-secondary"); secondaryPopup = new Popup(); secondaryPopup.setAutoHide(true); secondaryPopup.getContent().add(secondaryVK); } secondaryVK.chars=null; ArrayList secondaryList = new ArrayList(); // Add primary character if (!isSymbol) { if (key.letterChars != null && key.letterChars.length() > 0) { if (shiftDown || capsDown) { secondaryList.add(key.letterChars.toUpperCase()); } else { secondaryList.add(key.letterChars); } } } // Add secondary character if (key.altChars != null && key.altChars.length() > 0) { if (shiftDown || capsDown) { secondaryList.add(key.altChars.toUpperCase()); } else { secondaryList.add(key.altChars); } } // Add more letters if (key.moreChars != null && key.moreChars.length > 0) { if (isSymbol) { //Add non-letters for (String ch : key.moreChars) { if (!Character.isLetter(ch.charAt(0))) { secondaryList.add(ch); } } } else { //Add letters for (String ch : key.moreChars) { if (Character.isLetter(ch.charAt(0))) { if (shiftDown || capsDown) { secondaryList.add(ch.toUpperCase()); } else { secondaryList.add(ch); } } } } } boolean isMultiChar = false; for (String s : secondaryList) { if (s.length() > 1 ) { isMultiChar = true; } } secondaryVK.chars = secondaryList.toArray(new String[secondaryList.size()]); if (secondaryVK.chars.length > 1) { if (secondaryVK.getSkin() != null) { ((FXVKSkin)secondaryVK.getSkin()).rebuildSecondaryVK(); } secondaryVK.setAttachedNode(textInput); FXVKSkin primarySkin = (FXVKSkin)primaryVK.getSkin(); FXVKSkin secondarySkin = (FXVKSkin)secondaryVK.getSkin(); //Insets insets = secondarySkin.getInsets(); int nKeys = secondaryVK.chars.length; int nRows = (int)Math.floor(Math.sqrt(Math.max(1, nKeys - 2))); int nKeysPerRow = (int)Math.ceil(nKeys / (double)nRows); final double w = snappedLeftInset() + snappedRightInset() + nKeysPerRow * PREF_PORTRAIT_KEY_WIDTH * (isMultiChar ? 2 : 1) + (nKeysPerRow - 1) * GAP; final double h = snappedTopInset() + snappedBottomInset() + nRows * PREF_KEY_HEIGHT + (nRows-1) * GAP; secondaryVK.setPrefWidth(w); secondaryVK.setMinWidth(USE_PREF_SIZE); secondaryVK.setPrefHeight(h); secondaryVK.setMinHeight(USE_PREF_SIZE); Platform.runLater(() -> { // Position popup on screen Point2D nodePoint = com.sun.javafx.util.Utils.pointRelativeTo(key, w, h, HPos.CENTER, VPos.TOP, 5, -3, true); double x = nodePoint.getX(); double y = nodePoint.getY(); Scene scene = key.getScene(); x = Math.min(x, scene.getWindow().getX() + scene.getWidth() - w); secondaryPopup.show(key.getScene().getWindow(), x, y); }); } } else { closeSecondaryVK(); } } private List> loadBoard(String type) { List> tmpBoard = boardMap.get(type); if (tmpBoard != null) { return tmpBoard; } String boardFileName = type.substring(0,1).toUpperCase() + type.substring(1).toLowerCase() + "Board.txt"; try { tmpBoard = new ArrayList>(5); List keys = new ArrayList(20); InputStream boardFile = FXVKSkin.class.getResourceAsStream(boardFileName); BufferedReader reader = new BufferedReader(new InputStreamReader(boardFile)); String line; // A pointer to the current column. This will be incremented for every string // of text, or space. int c = 0; // The col at which the key will be placed int col = 0; // The number of columns that the key will span int colSpan = 1; // Whether the "chars" is an identifier, like $shift or $SymbolBoard, etc. boolean identifier = false; // The textual content of the Key List charsList = new ArrayList(10); while ((line = reader.readLine()) != null) { if (line.length() == 0 || line.charAt(0) == '#') { continue; } // A single line represents a single row of buttons for (int i=0; i(10); identifier = false; } else if (ch == ']') { String chars = ""; String alt = null; String[] moreChars = null; for (int idx = 0; idx < charsList.size(); idx++) { charsList.set(idx, FXVKCharEntities.get(charsList.get(idx))); } int listSize = charsList.size(); if (listSize > 0) { chars = charsList.get(0); if (listSize > 1) { alt = charsList.get(1); if (listSize > 2) { moreChars = charsList.subList(2, listSize).toArray(new String[listSize - 2]); } } } // End of a key colSpan = c - col; Key key; if (identifier) { if ("$shift".equals(chars)) { key = new KeyboardStateKey("", null, "shift") { @Override protected void release() { pressShift(); } @Override public void update(boolean capsDown, boolean shiftDown, boolean isSymbol) { if (isSymbol) { this.setDisable(true); this.setVisible(false); } else { if (capsDown) { icon.getStyleClass().remove("shift-icon"); icon.getStyleClass().add("capslock-icon"); } else { icon.getStyleClass().remove("capslock-icon"); icon.getStyleClass().add("shift-icon"); } this.setDisable(false); this.setVisible(true); } } }; key.getStyleClass().add("shift"); } else if ("$SymbolABC".equals(chars)) { key = new KeyboardStateKey("!#123", "ABC", "symbol") { @Override protected void release() { pressSymbolABC(); } }; } else if ("$backspace".equals(chars)) { key = new KeyCodeKey("backspace", "\b", KeyCode.BACK_SPACE) { @Override protected void press() { if (KEY_REPEAT_RATE > 0) { clearShift(); sendKeyEvents(); repeatKey = this; repeatInitialDelay.playFromStart(); } else { super.press(); } } @Override protected void release() { if (KEY_REPEAT_RATE > 0) { repeatInitialDelay.stop(); repeatSubsequentDelay.stop(); } else { super.release(); } } }; key.getStyleClass().add("backspace"); } else if ("$enter".equals(chars)) { key = new KeyCodeKey("enter", "\n", KeyCode.ENTER); key.getStyleClass().add("enter"); } else if ("$tab".equals(chars)) { key = new KeyCodeKey("tab", "\t", KeyCode.TAB); } else if ("$space".equals(chars)) { key = new CharKey(" ", " ", null, "space"); } else if ("$clear".equals(chars)) { key = new SuperKey("clear", ""); } else if ("$.org".equals(chars)) { key = new SuperKey(".org", ".org"); } else if ("$.com".equals(chars)) { key = new SuperKey(".com", ".com"); } else if ("$.net".equals(chars)) { key = new SuperKey(".net", ".net"); } else if ("$oracle.com".equals(chars)) { key = new SuperKey("oracle.com", "oracle.com"); } else if ("$gmail.com".equals(chars)) { key = new SuperKey("gmail.com", "gmail.com"); } else if ("$hide".equals(chars)) { key = new KeyboardStateKey("hide", null, "hide") { @Override protected void release() { isVKHidden = true; startSlideOut(false); // Restore window position if (vkAdjustWindow) { restoreWindowPosition(attachedNode); } } }; key.getStyleClass().add("hide"); } else if ("$undo".equals(chars)) { key = new SuperKey("undo", ""); } else if ("$redo".equals(chars)) { key = new SuperKey("redo", ""); } else { //Unknown Key key = null; } } else { key = new CharKey(chars, alt, moreChars); } if (key != null) { key.col = col; key.colSpan = colSpan; for (String sc : key.getStyleClass()) { key.text.getStyleClass().add(sc + "-text"); key.altText.getStyleClass().add(sc + "-alttext"); key.icon.getStyleClass().add(sc + "-icon"); } if (chars != null && chars.length() > 1) { key.text.getStyleClass().add("multi-char-text"); } if (alt != null && alt.length() > 1) { key.altText.getStyleClass().add("multi-char-text"); } keys.add(key); } } else { // Normal textual characters. Read all the way up to the // next ] or space for (int j=i; j(20); } reader.close(); boardMap.put(type, tmpBoard); return tmpBoard; } catch (Exception e) { e.printStackTrace(); return Collections.emptyList(); } } }