changeset 2197:c4db0c70f77d

New simplified Virtual Keyboard implementation
author Seeon Birger <seeon.birger@oracle.com>
date Wed, 09 Jan 2013 18:22:50 +0200
parents 9b32f08b395a
children 2b6f25bf2b7b
files javafx-ui-controls/src/com/sun/javafx/scene/control/skin/AsciiBoard.txt javafx-ui-controls/src/com/sun/javafx/scene/control/skin/EmailBoard.txt javafx-ui-controls/src/com/sun/javafx/scene/control/skin/FXVK.java javafx-ui-controls/src/com/sun/javafx/scene/control/skin/FXVKSkin.java javafx-ui-controls/src/com/sun/javafx/scene/control/skin/SymbolBoard.txt javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/embedded.css javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/fxvk.css javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/backspace-icon.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/capslock-icon.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/enter-icon.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/shift-icon.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-dark-pressed.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-dark.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-hide.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-light-pressed.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-light.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-medium-pressed.png javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-medium.png
diffstat 18 files changed, 635 insertions(+), 850 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/AsciiBoard.txt	Wed Jan 09 18:22:50 2013 +0200
@@ -0,0 +1,5 @@
+[`|~ ][1|! ][2|@ ][3|# ][4|\$ ][5|% ][6|^ ][7|& ][8|* ][9|( ][0|) ][-|_ ][=|+ ][$backspace  ]
+[$tab  ][q ][w ][e ][r ][t ][y ][u ][i ][o ][p ][\[|{ ][\]|} ][\\|\| ]
+[$caps   ][a ][s ][d ][f ][g ][h ][j ][k ][l ][;|: ]['|" ][$enter  ]
+[$shift    ][z ][x ][c ][v ][b ][n ][m ][,|< ][.|> ][/|? ][$shift   ]
+[$hide   ][$SymbolBoard  ][$space               ][$SymbolBoard  ][$hide  ]
\ No newline at end of file
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/EmailBoard.txt	Wed Jan 09 18:22:50 2013 +0200
@@ -0,0 +1,5 @@
+[`|~ ][1|! ][2|, ][3|# ][4|\$ ][5|% ][6|^ ][7|& ][8|* ][9|( ][0|) ][-|_ ][=|+ ][$backspace  ]
+[$tab  ][q ][w ][e ][r ][t ][y ][u ][i ][o ][p ][\[|{ ][\]|} ][\\|\| ]
+[$caps   ][a ][s ][d ][f ][g ][h ][j ][k ][l ][;|: ]['|" ][$enter  ]
+[$shift    ][z ][x ][c ][v ][b ][n ][m ][@|< ][.|> ][/|? ][$shift   ]
+[$hide ][$.org  ][$.net  ][$space           ][$.com  ][$oracle.com   ][$hide ]
\ No newline at end of file
--- a/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/FXVK.java	Wed Jan 09 15:29:55 2013 +0200
+++ b/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/FXVK.java	Wed Jan 09 18:22:50 2013 +0200
@@ -49,18 +49,48 @@
 import com.sun.javafx.css.CssMetaData;
 import com.sun.javafx.css.converters.BooleanConverter;
 
+import javafx.beans.property.ObjectProperty;
+import javafx.beans.property.SimpleObjectProperty;
+import javafx.event.EventHandler;
+import javafx.scene.control.Control;
+import javafx.scene.input.KeyEvent;
+
+
 public class FXVK extends Control {
 
+    public enum Type {
+        TEXT,
+        NUMERIC,
+        EMAIL,
+    }
+
+    private final ObjectProperty<Type> type = new SimpleObjectProperty<Type>(this, "type");
+    public final Type getType() { return type.get(); }
+    public final void setType(Type value) { type.set(value); }
+    public final ObjectProperty<Type> typeProperty() { return type; }
+
+
+    private final ObjectProperty<EventHandler<KeyEvent>> onAction =
+            new SimpleObjectProperty<EventHandler<KeyEvent>>(this, "onAction");
+    public final void setOnAction(EventHandler<KeyEvent> value) { onAction.set(value); }
+    public final EventHandler<KeyEvent> getOnAction() { return onAction.get(); }
+    public final ObjectProperty<EventHandler<KeyEvent>> onActionProperty() { return onAction; }
+
+
     final static String[] VK_TYPE_NAMES = new String[] { "text", "numeric", "url", "email" };
     public final static String VK_TYPE_PROP_KEY = "vkType";
 
     String[] chars;
 
-    FXVK() {
-        setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
-        getStyleClass().setAll(DEFAULT_STYLE_CLASS);
+    public FXVK() {
+        this(Type.TEXT);
     }
 
+    public FXVK(Type type) {
+        this.type.set(type);
+        setNodeOrientation(NodeOrientation.LEFT_TO_RIGHT);
+        getStyleClass().add(DEFAULT_STYLE_CLASS);
+    }
 
     final ObjectProperty<Node> attachedNodeProperty() {
         if (attachedNode == null) {
@@ -76,46 +106,29 @@
         }
         return attachedNode;
     }
+
     private ObjectProperty<Node> attachedNode;
     final void setAttachedNode(Node value) { attachedNodeProperty().setValue(value); }
     final Node getAttachedNode() { return attachedNode == null ? null : attachedNode.getValue(); }
-
-
-    int vkType;
     static FXVK vk;
-    private static HashMap<Integer, FXVK> vkMap = new HashMap<Integer, FXVK>();
 
     public static void attach(final Node textInput) {
         int type = 0;
         Object typeValue = textInput.getProperties().get(VK_TYPE_PROP_KEY);
+        String typeStr = "";
         if (typeValue instanceof String) {
-            String typeStr = ((String)typeValue).toLowerCase();
-            for (int i = 0; i < VK_TYPE_NAMES.length; i++) {
-                if (typeStr.equals(VK_TYPE_NAMES[i])) {
-                    type = i;
-                    break;
-                }
-            }
+            typeStr = ((String)typeValue).toLowerCase();
         }
 
-        vk = vkMap.get(type);
         if (vk == null) {
-            vk = new FXVK();
-            vk.vkType = type;
+            vk = new FXVK(Type.TEXT);
             vk.setSkin(new FXVKSkin(vk));
-            vkMap.put(type, vk);
-        }
-
-        for (FXVK v : vkMap.values()) {
-            if (v != vk) {
-                v.setAttachedNode(null);
-            }
         }
         vk.setAttachedNode(textInput);
     }
 
     public static void detach() {
-        for (FXVK vk : vkMap.values()) {
+        if (vk != null) {
             vk.setAttachedNode(null);
         }
     }
--- a/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/FXVKSkin.java	Wed Jan 09 15:29:55 2013 +0200
+++ b/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/FXVKSkin.java	Wed Jan 09 18:22:50 2013 +0200
@@ -1,4 +1,4 @@
-/*
+ /*
  * Copyright (c) 2010, 2011, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
@@ -53,6 +53,8 @@
 import javafx.geometry.Pos;
 import javafx.geometry.Rectangle2D;
 import javafx.geometry.VPos;
+import javafx.geometry.Side;
+
 import javafx.scene.Group;
 import javafx.scene.Node;
 import javafx.scene.Parent;
@@ -62,6 +64,8 @@
 import javafx.scene.input.KeyEvent;
 import javafx.scene.input.MouseEvent;
 import javafx.scene.layout.*;
+import javafx.scene.image.*;
+
 import javafx.stage.*;
 import javafx.util.Duration;
 
@@ -76,8 +80,60 @@
 
 import static com.sun.javafx.scene.control.skin.resources.EmbeddedResources.*;
 
+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 javafx.event.EventHandler;
+import javafx.event.EventType;
+import javafx.geometry.Insets;
+import javafx.geometry.VPos;
+import javafx.scene.input.KeyCode;
+import javafx.scene.input.KeyEvent;
+import javafx.scene.input.MouseEvent;
+import javafx.scene.layout.Region;
+import javafx.scene.text.Text;
+import com.sun.javafx.scene.control.behavior.BehaviorBase;
+import java.net.URL;
+
+
 public class FXVKSkin extends BehaviorSkinBase<FXVK, BehaviorBase<FXVK>> {
 
+    private static final int GAP = 6;
+
+    private List<List<Key>> board;
+    private int numCols;
+
+    private boolean capsDown = false;
+    private boolean shiftDown = false;
+
+    void clearShift() {
+        shiftDown = false;
+        updateKeys();
+    }
+
+    void pressShift() {
+        shiftDown = !shiftDown;
+        updateKeys();
+    }
+
+    void pressCaps() {
+        capsDown = !capsDown;
+        shiftDown = false;
+        updateKeys();
+    }
+
+    private void updateKeys() {
+        for (List<Key> row : board) {
+            for (Key key : row) {
+                key.update(capsDown, shiftDown);
+            }
+        }
+    }
+
+
     /**
      * If true, places the virtual keyboard(s) in a new root wrapper
      * on the scene instead of in a popup. The children of the wrapper
@@ -102,7 +158,6 @@
     private final static boolean USE_SECONDARY_POPUP = false;
 
     private static Region oldRoot;
-    private static RootWrapper newRoot;
     private static Timeline slideRootTimeline;
 
     private static Popup vkPopup;
@@ -114,12 +169,10 @@
 
     private static FXVK secondaryVK;
     private static Timeline secondaryVKDelay;
-    private static CharKey secondaryVKKey;
 
     private Node attachedNode;
 
     FXVK fxvk;
-    Control[][] keyRows;
 
     enum State { NORMAL, SHIFTED, SHIFT_LOCK, NUMERIC; };
 
@@ -127,8 +180,6 @@
 
     static final double VK_WIDTH = 640;
     static final double VK_HEIGHT = 243;
-//     static final double VK_WIDTH = 480;
-//     static final double VK_HEIGHT = 326;
     static final double VK_PORTRAIT_HEIGHT = 326;
     static final double VK_SLIDE_MILLIS = 250;
     static final double PREF_KEY_WIDTH = 56;
@@ -138,11 +189,6 @@
     double keyWidth = PREF_KEY_WIDTH;
     double keyHeight = PREF_KEY_HEIGHT;
 
-    private ShiftKey shiftKey;
-    private SymbolKey symbolKey;
-
-    private VBox vbox;
-
     // Proxy for read-only Window.yProperty() so we can animate.
     private static DoubleProperty winY = new SimpleDoubleProperty();
     static {
@@ -163,10 +209,23 @@
     });
 
 
+    @Override protected void handleControlPropertyChanged(String propertyReference) {
+        // With Java 8 (or is it 7?) I can do switch on strings instead
+        if (propertyReference == "type") {
+            // The type has changed, so we will need to rebuild the entire keyboard.
+            // This happens whenever the user switches from one keyboard layout to
+            // another, such as by pressing the "ABC" key on a numeric layout.
+            rebuild();
+        }
+    }
+
     public FXVKSkin(final FXVK fxvk) {
         super(fxvk, new BehaviorBase<FXVK>(fxvk));
         this.fxvk = fxvk;
 
+        registerChangeListener(fxvk.typeProperty(), "type");
+        rebuild();
+
         fxvk.setFocusTraversable(false);
 
         fxvk.attachedNodeProperty().addListener(new InvalidationListener() {
@@ -179,53 +238,20 @@
                     return;
                 }
 
-                if (inScene) {
-                    if (oldRoot != null) {
-                        translatePane(oldRoot, 0);
-                    }
-                }
+                if (attachedNode != null) {
+                    final Scene scene = attachedNode.getScene();
+                    //fxvk.getStyleClass().setAll("virtual-keyboard");
 
-                if (attachedNode != null) {
-                    if (keyRows == null) {
-                        createKeys();
-                    }
-
-                    final Scene scene = attachedNode.getScene();
                     fxvk.setVisible(true);
 
                     if (fxvk != secondaryVK) {
-                        if (secondaryVKDelay == null) {
-                            secondaryVKDelay = new Timeline();
-                        }
-                        KeyFrame kf = new KeyFrame(Duration.millis(500), new EventHandler<ActionEvent>() {
-                            @Override public void handle(ActionEvent event) {
-                                if (secondaryVKKey != null) {
-                                    showSecondaryVK(secondaryVKKey);
-                                }
-                            }
-                        });
-                        secondaryVKDelay.getKeyFrames().setAll(kf);
-
-                      if (inScene) {
-                            if (newRoot == null) {
-                                oldRoot = (Region)scene.getRoot();
-                                newRoot = new RootWrapper(oldRoot);
-                                scene.setRoot(newRoot);
-                            }
-
-                            newRoot.vkGroup.getChildren().setAll(fxvk);
-                            newRoot.updateTimelines();
-
-                            Bounds bounds = newRoot.vkGroup.getLayoutBounds();
-                            double y = newRoot.vkGroup.getLayoutY();
-                            if (bounds.getHeight() > 0 &&
-                                (y == 0 || y > scene.getHeight() - bounds.getHeight())) {
-                                slideOutTimeline.stop();
-                                slideInTimeline.playFromStart();
-                            }
-                      } else {
                         if (vkPopup == null) {
                             vkPopup = new Popup();
+                            vkPopup.setAutoFix(false);
+
+                            Scene popupScene = vkPopup.getScene();
+                            popupScene.getStylesheets().add(getClass().getResource("caspian/fxvk.css").toExternalForm());
+
 
                             // RT-21860 - This is causing
                             // IllegalStateException: The window must be focused when calling grabFocus()
@@ -239,11 +265,12 @@
                                 com.sun.javafx.Utils.getScreen(attachedNode).getBounds().getHeight();
                             double screenVisualHeight =
                                 com.sun.javafx.Utils.getScreen(attachedNode).getVisualBounds().getHeight();
+
                             screenVisualHeight = Math.min(screenHeight, screenVisualHeight + 4 /*??*/);
 
                             slideInTimeline.getKeyFrames().setAll(
                                 new KeyFrame(Duration.millis(VK_SLIDE_MILLIS),
-                                             new KeyValue(winY, screenVisualHeight - fxvk.prefHeight(-1),
+                                             new KeyValue(winY, screenHeight - VK_HEIGHT,
                                                           Interpolator.EASE_BOTH)));
                             slideOutTimeline.getKeyFrames().setAll(
                                 new KeyFrame(Duration.millis(VK_SLIDE_MILLIS),
@@ -257,6 +284,8 @@
                                 public void run() {
                                     Rectangle2D screenBounds =
                                         com.sun.javafx.Utils.getScreen(attachedNode).getBounds();
+
+                                        
                                     vkPopup.show(attachedNode,
                                                  (screenBounds.getWidth() - fxvk.prefWidth(-1)) / 2,
                                                  screenBounds.getHeight() - fxvk.prefHeight(-1));
@@ -277,25 +306,11 @@
                             winY.set(vkPopup.getY());
                             slideInTimeline.playFromStart();
                         }
-                      }
-
-                        if (inScene) {
-                            Platform.runLater(new Runnable() {
-                                public void run() {
-                                    if (attachedNode != null) {
-                                        double nodeBottom =
-                                            attachedNode.localToScene(attachedNode.getBoundsInLocal()).getMaxY() + 2;
-                                        if (fxvk.getLayoutY() > 0 && nodeBottom > fxvk.getLayoutY()) {
-                                            translatePane(oldRoot, fxvk.getLayoutY() - nodeBottom);
-                                        }
-                                    }
-                                }
-                            });
-                        }
 
                         if (oldNode == null || oldNode.getScene() != attachedNode.getScene()) {
-                            if (!inScene) { 
-                                fxvk.setPrefWidth(VK_WIDTH);
+                            if (!inScene) {
+                                double width = com.sun.javafx.Utils.getScreen(attachedNode).getBounds().getWidth();
+                                fxvk.setPrefWidth(width);
                             }
                             fxvk.setMinWidth(USE_PREF_SIZE);
                             fxvk.setMaxWidth(USE_PREF_SIZE);
@@ -305,6 +320,7 @@
                     }
                 } else {
                     if (fxvk != secondaryVK) {
+
                         slideInTimeline.stop();
                         if (!inScene) {
                             winY.set(vkPopup.getY());
@@ -322,704 +338,463 @@
         });
     }
 
-    private static void translatePane(Parent pane, double y) {
-        if (slideRootTimeline == null) {
-            slideRootTimeline = new Timeline();
-        } else {
-            slideRootTimeline.stop();
+    /**
+     * Replaces all children of this VirtualKeyboardSkin based on the keyboard
+     * type set on the VirtualKeyboard.
+     */
+    private void rebuild() {
+        String boardName;
+        FXVK.Type type = getSkinnable().getType();
+        switch (type) {
+            case NUMERIC:
+                boardName = "SymbolBoard";
+                break;
+            case TEXT:
+                boardName = "AsciiBoard";
+                break;
+            case EMAIL:
+                boardName = "EmailBoard";
+                break;
+            default:
+                throw new AssertionError("Unhandled Virtual Keyboard type");
         }
 
-        slideRootTimeline.getKeyFrames().setAll(
-            new KeyFrame(Duration.millis(VK_SLIDE_MILLIS),
-                         new KeyValue(pane.translateYProperty(), y, Interpolator.EASE_BOTH)));
-        slideRootTimeline.playFromStart();
+        board = loadBoard(boardName);
+        getChildren().clear();
+        numCols = 0;
+        for (List<Key> row : board) {
+            for (Key key : row) {
+                numCols = Math.max(numCols, key.col + key.colSpan);
+            }
+            getChildren().addAll(row);
+        }
     }
 
-    private void createKeys() {
-        getChildren().clear();
+    // 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) {
+        final Insets insets = getInsets();
+        return insets.getLeft() + (56 * numCols) + insets.getRight();
+    }
 
-        if (fxvk.chars != null) {
-            // Secondary popup
-            int nKeys = fxvk.chars.length;
-            if (nKeys > 1) {
-                // Reorder to make letter keys appear before symbols
-                String[] array = new String[nKeys];
-                int ind = 0;
-                for (String str : fxvk.chars) {
-                    if (Character.isLetter(str.charAt(0))) {
-                        array[ind++] = str;
-                    }
+    // Pref height is just some value. This isn't overly important.
+    @Override protected double computePrefHeight(double width) {
+        final Insets insets = getInsets();
+        return insets.getTop() + (80 * 5) + insets.getBottom();
+    }
+
+    // Lays the buttons comprising the current keyboard out. The first row is always
+    // a "short" row (about 2/3 in height of a normal row), followed by 4 normal rows.
+    @Override
+    protected void layoutChildren(double contentX, double contentY, double contentWidth, double contentHeight) {
+        // I have fixed width columns, all the same.
+        final double colWidth = ((contentWidth - ((numCols - 1) * GAP)) / numCols);
+        double rowHeight = ((contentHeight - (4 * GAP)) / 5); // 5 rows per keyboard
+        // The first row is 2/3 the height
+        double firstRowHeight = rowHeight * .666;
+        rowHeight += ((rowHeight * .333) / 4);
+
+        double rowY = contentY;
+        double h = firstRowHeight;
+        for (List<Key> row : board) {
+            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, h);
+            }
+            rowY += h + GAP;
+            h = rowHeight;
+        }
+    }
+
+
+    /**
+     * 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 Region icon;
+
+        protected Key() {
+            icon = new Region();
+            text = new Text();
+            text.setTextOrigin(VPos.TOP);
+            getChildren().setAll(text, icon);
+            getStyleClass().setAll("key");
+            addEventHandler(MouseEvent.ANY, new EventHandler<MouseEvent>() {
+                @Override public void handle(MouseEvent event) {
+                    if (event.getEventType() == MouseEvent.MOUSE_PRESSED)
+                        press();
+                    else if (event.getEventType() == MouseEvent.MOUSE_RELEASED)
+                        release();
                 }
-                for (String str : fxvk.chars) {
-                    if (!Character.isLetter(str.charAt(0))) {
-                        array[ind++] = str;
-                    }
-                }
-
-                int nRows = (int)Math.floor(Math.sqrt(Math.max(1, nKeys - 2)));
-                int nKeysPerRow = (int)Math.ceil(nKeys / (double)nRows);
-                keyRows = new Control[nRows][];
-                for (int i = 0; i < nRows; i++) {
-                    keyRows[i] =
-                        makeKeyRow((String[])Arrays.copyOfRange(array, i * nKeysPerRow,
-                                                                Math.min((i + 1) * nKeysPerRow, nKeys)));
-                }
-            } else {
-                keyRows = new Control[0][];
-            }
-        } else {
-            // Read keyboard layout from resource bundle
-            ArrayList<Control[]> rows = new ArrayList<Control[]>();
-            ArrayList<String> row = new ArrayList<String>();
-            ArrayList<Double> keyWidths = new ArrayList<Double>();
-            String typeString = FXVK.VK_TYPE_NAMES[fxvk.vkType];
-            int r = 0;
-            try {
-                String format = "FXVK."+typeString+".row%d.key%02d";
-                while (getBundle().containsKey(String.format(format, ++r, 1))) {
-                    int c = 0;
-                    String keyChars;
-                    while (getBundle().containsKey(String.format(format, r, ++c))) {
-                        row.add(getString(String.format(format, r, c)));
-                        Double w = -1.0;
-                        String widthLookup = String.format(format+".width", r, c);
-                        if (getBundle().containsKey(widthLookup)) {
-                            try {
-                                w = new Double(getString(widthLookup));
-                            } catch (NumberFormatException ex) {
-                                System.err.println(widthLookup+"="+getString(widthLookup));
-                                System.err.println(ex);
-                            }
-                        }
-                        keyWidths.add(w);
-                    }
-                    rows.add(makeKeyRow(row, keyWidths));
-                    row.clear();
-                    keyWidths.clear();
-                }
-            } catch (Exception ex) {
-                ex.printStackTrace();
-            }
-            keyRows = rows.toArray(new Control[rows.size()][]);
+            });
+        }
+        protected void press() { }
+        protected void release() {
+            clearShift();
         }
 
-        vbox = new VBox();
-        vbox.setId("vbox");
-        getChildren().add(vbox);
+        public void update(boolean capsDown, boolean shiftDown) { }
 
-        //double primaryFontSize = 16 * keyWidth / PREF_KEY_WIDTH;
-        //double secondaryFontSize = 8 * keyWidth / PREF_KEY_WIDTH;
+        @Override protected void layoutChildren() {
+            final Insets insets = getInsets();
+            final double left = insets.getLeft();
+            final double top = insets.getTop();
+            final double width = getWidth() - left - insets.getRight();
+            final double height = getHeight() - top - insets.getBottom();
 
-        for (Control[] row : keyRows) {
-            HBox hbox = new HBox();
-            hbox.setId("hbox");
-            // Primary keyboard has centered keys, secondary has left aligned keys.
-            hbox.setAlignment((fxvk.chars != null) ? Pos.CENTER_LEFT : Pos.CENTER);
-            vbox.getChildren().add(hbox);
-            for (Control c : row) {
-                if (enableCaching) {
-                    c.setCache(true);
-                }
-                hbox.getChildren().add(c);
-                HBox.setHgrow(c, Priority.ALWAYS);
-                if (c instanceof Key) {
-                    Key key = (Key)c;
-                    int textLen = key.getText().length();
-                    if (textLen == 1 || !key.getClass().getSimpleName().equals("CharKey")) {
-                        //key.setStyle("-fx-font-size: "+primaryFontSize+"px;");
-                    } else {
-                        //key.setStyle("-fx-font-size: "+(primaryFontSize* Math.min(1.0, 3.0/textLen))+"px;");
-                        key.setGraphicTextGap(key.getGraphicTextGap() + 2*textLen);
-                    }
-                    if (key.getGraphic() instanceof Label) {
-                        //((Label)key.getGraphic()).setStyle("-fx-font-size: "+secondaryFontSize+"px;");
-                    }
-                }
+            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);
+
+            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 {
+        protected String chars = "";
+
+        protected void press() {
+            Node target = fxvk.getAttachedNode();
+            if (target instanceof EventTarget) {
+                target.fireEvent(event(KeyEvent.KEY_PRESSED));
+            }
+        }
+        protected void release() {
+            Node target = fxvk.getAttachedNode();
+            if (target instanceof EventTarget) {
+                target.fireEvent(event(KeyEvent.KEY_TYPED));
+                target.fireEvent(event(KeyEvent.KEY_RELEASED));
+            }
+            super.release();
+        }
+
+        protected KeyEvent event(EventType<KeyEvent> type) {
+            try {
+                Field fld = FXRobotHelper.class.getDeclaredField("inputAccessor");
+                fld.setAccessible(true);
+                FXRobotInputAccessor inputAccessor = (FXRobotInputAccessor)fld.get(null);
+
+                return inputAccessor.createKeyEvent(type, KeyCode.UNDEFINED, chars, "",
+                                      shiftDown, false, false, false);
+            } catch (Exception e) {
+                System.err.println(e);
+            }
+
+            return null;
+        }
+    }
+
+    /**
+     * A key used for letters a-z, and handles responding to the shift & caps lock
+     * keys, such that lowercase or uppercase letters are entered.
+     */
+    private class LetterKey extends TextInputKey {
+        private LetterKey(String letter) {
+            this.chars = letter;
+            text.setText(this.chars);
+        }
+
+        public void update(boolean capsDown, boolean shiftDown) {
+            final boolean capital = capsDown || shiftDown;
+            if (capital) {
+                this.chars = this.chars.toUpperCase();
+                text.setText(this.chars);
+            } else {
+                this.chars = this.chars.toLowerCase();
+                text.setText(this.chars);
             }
         }
     }
 
+    /**
+     * A key which has a number or symbol on it, such as the "1" key which can also
+     * enter the ! character when shift is pressed. Also used for purely symbolic
+     * keys such as [.
+     */
+    private class SymbolKey extends TextInputKey {
+        private final String letterChars;
+        private final String altChars;
 
-    private Control[] makeKeyRow(String... obj) {
-        return makeKeyRow(Arrays.asList(obj), null);
-    }
-
-    private Control[] makeKeyRow(List<String> keyList, List<Double> widths) {
-        Control[] keyRow = new Control[keyList.size()];
-        for (int i = 0; i < keyRow.length; i++) {
-            String str = keyList.get(i);
-            Double w = 1.0;
-            if (widths != null && widths.get(i) > 0) {
-                w = widths.get(i);
-            }
-            if ("BACKSPACE".equals(str)) {
-                CommandKey backspaceKey = new CommandKey("\u232b", BACK_SPACE, w);
-                backspaceKey.setId("backspace-key");
-// Workaround until we can load -fx-graphic from caspian.css
-setIcon(backspaceKey, "fxvk-backspace-button.png");
-                keyRow[i] = backspaceKey;
-            } else if ("ENTER".equals(str)) {
-                CommandKey enterKey = new CommandKey("\u21b5", ENTER, w);
-                enterKey.setId("enter-key");
-// Workaround until we can load -fx-graphic from caspian.css
-setIcon(enterKey, "fxvk-enter-button.png");
-                keyRow[i] = enterKey;
-            } else if ("SHIFT".equals(str)) {
-                shiftKey = new ShiftKey(w);
-                shiftKey.setId("shift-key");
-// Workaround until we can load -fx-graphic from caspian.css
-setIcon(shiftKey, "fxvk-shift-button.png");
-                keyRow[i] = shiftKey;
-            } else if ("SYM".equals(str)) {
-                symbolKey = new SymbolKey("!#123 ABC", w);
-                symbolKey.setId("symbol-key");
-                keyRow[i] = symbolKey;
-            } else {
-                keyRow[i] = new CharKey((String)keyList.get(i), w);
-            }
-        }
-        return keyRow;
-    }
-
-    private void setState(State state) {
-        this.state = state;
-
-        shiftKey.setPressState(state == State.SHIFTED || state == State.SHIFT_LOCK);
-        shiftKey.setDisable(state == State.NUMERIC);
-        shiftKey.setId((state == State.SHIFT_LOCK) ? "capslock-key" : "shift-key");
-
-// Workaround until we can load -fx-graphic from caspian.css
-switch (state) {
-    case NUMERIC: setIcon(shiftKey, null); break;
-    case SHIFT_LOCK: setIcon(shiftKey, "fxvk-capslock-button.png"); break;
-    default: setIcon(shiftKey, "fxvk-shift-button.png");
-}
-        if (fxvk == secondaryVK) {
-            ((FXVKSkin)primaryVK.getSkin()).updateLabels();
-        } else {
-            updateLabels();
-        }
-    }
-
-// Workaround until we can load -fx-graphic from caspian.css
-private void setIcon(Key key, String fileName) {
-    if (fileName != null) {
-        String url = getClass().getResource("caspian/"+fileName).toExternalForm();
-        key.setGraphic(new javafx.scene.image.ImageView(url));
-    } else {
-        key.setGraphic(null);
-    }
-}
-
-    private void updateLabels() {
-        for (Control[] row : keyRows) {
-            for (Control button : row) {
-                if (button instanceof CharKey) {
-                    CharKey key = (CharKey)button;
-                    String txt = key.chars[0];
-                    String alt = (key.chars.length > 1) ? key.chars[1] : "";
-                    if (key.chars.length > 1 && state == State.NUMERIC) {
-                        txt = key.chars[1];
-                        if (key.chars.length > 2 && !Character.isLetter(key.chars[2].charAt(0))) {
-                            alt = key.chars[2];
-                        } else {
-                            alt = "";
-                        }
-                    } else if (state == State.SHIFTED || state == State.SHIFT_LOCK) {
-                        txt = txt.toUpperCase();
-                    }
-                    key.setText(txt);
-                    if (key.graphic != null) {
-                        key.graphic.setText(alt);
-                    }
-                }
-            }
-        }
-        if (symbolKey != null) {
-            symbolKey.setText(symbolKey.chars[(state == State.NUMERIC) ? 1 : 0]);
-        }
-    }
-
-    private void fireKeyEvent(Node target, EventType<? extends KeyEvent> eventType,
-                           KeyCode keyCode, String keyChar, String keyText,
-                           boolean shiftDown, boolean controlDown,
-                           boolean altDown, boolean metaDown) {
-        try {
-            Field fld = FXRobotHelper.class.getDeclaredField("inputAccessor");
-            fld.setAccessible(true);
-            FXRobotInputAccessor inputAccessor = (FXRobotInputAccessor)fld.get(null);
-            target.fireEvent(inputAccessor.createKeyEvent(eventType,
-                                                          keyCode, keyChar, keyText,
-                                                          shiftDown, controlDown,
-                                                          altDown, metaDown));
-        } catch (Exception e) {
-            System.err.println(e);
-        }
-    }
-
-
-
-    private class Key extends Button {
-        private double keyWidth;
-
-        private Key(String text, double keyWidth) {
-            super(text);
-
-            this.keyWidth = keyWidth;
-
-            getStyleClass().add("key");
-            setFocusTraversable(false);
-
-            setMinHeight(USE_PREF_SIZE);
-            setPrefHeight(keyHeight);
+        private SymbolKey(String letter, String alt) {
+            this.chars = letter;
+            this.letterChars = this.chars;
+            this.altChars = alt;
+            text.setText(this.letterChars);
         }
 
-    }
-
-    private class CharKey extends Key {
-        String str;
-        String[] chars;
-        Label graphic;
-
-        EventHandler<ActionEvent> actionHandler = new EventHandler<ActionEvent>() {
-            @Override public void handle(ActionEvent e) {
-                if (fxvk != secondaryVK && secondaryPopup != null && secondaryPopup.isShowing()) {
-                    return;
-                }
-
-                Node target = fxvk.getAttachedNode();
-                if (target instanceof EventTarget) {
-                    String txt = getText();
-                    if (txt.length() > 1 && txt.contains(" ")) {
-                        //txt = txt.split(" ")[shift ? 1 : 0];
-                        txt = txt.split(" ")[0];
-                    }
-                    for (int i = 0; i < txt.length(); i++) {
-                        String str = txt.substring(i, i+1);
-                        fireKeyEvent(target, KeyEvent.KEY_TYPED, null, str, str,
-                                  state == State.SHIFTED, false, false, false);
-                    }
-
-                    if (state == State.SHIFTED) {
-                        setState(State.NORMAL);
-                    }
-                }
-
-                if (fxvk == secondaryVK) {
-                    showSecondaryVK(null);
-                }
-            }
-        };
-
-        CharKey(String str, double width) {
-            super(null, width);
-
-            this.str = str;
-            setOnAction(actionHandler);
-
-            if (fxvk != secondaryVK) {
-                setOnMousePressed(new EventHandler<MouseEvent>() {
-                    @Override public void handle(MouseEvent event) {
-                        showSecondaryVK(null);
-                        secondaryVKKey = CharKey.this;
-                        secondaryVKDelay.playFromStart();
-                    }
-                });
-
-                setOnMouseReleased(new EventHandler<MouseEvent>() {
-                    @Override public void handle(MouseEvent event) {
-                        secondaryVKDelay.stop();
-                    }
-                });
-            }
-
-            if (str.length() == 1) {
-                chars = new String[] { str };
+        public void update(boolean capsDown, boolean shiftDown) {
+            if (shiftDown && altChars != null) {
+                this.chars = altChars;
+                text.setText(this.chars);
             } else {
-                chars = str.split(" ");
-                for (int i = 0; i < chars.length; i++) {
-                    chars[i] = FXVKCharEntities.get(chars[i]);
-                }
-            }
-            setContentDisplay(ContentDisplay.TOP);
-            setGraphicTextGap(-16);
-            setText(chars[0]);
-            setId(chars[0]);
-            if (getText().length() > 1) {
-                getStyleClass().add("multi-char-key");
-            }
-
-            graphic = new Label((chars.length > 1) ? chars[1] : " ");
-            graphic.setPrefWidth(keyWidth - 6);
-            graphic.setMinWidth(USE_PREF_SIZE);
-            graphic.setPrefHeight(keyHeight / 2 - 8);
-            setGraphic(graphic);
-        }
-    }
-
-    private class CommandKey extends Key {
-        KeyCode code;
-
-        EventHandler<ActionEvent> actionHandler = new EventHandler<ActionEvent>() {
-            @Override public void handle(ActionEvent e) {
-                showSecondaryVK(null);
-                Node target = fxvk.getAttachedNode();
-                if (target instanceof EventTarget) {
-                    String txt = getText();
-                    fireKeyEvent(target, KeyEvent.KEY_PRESSED, code, null, null,
-                              false, false, false, false);
-                    if (state == State.SHIFTED) {
-                        setState(State.NORMAL);
-                    }
-                }
-            }
-        };
-
-        CommandKey(String label, KeyCode code, double width) {
-            super(label, width);
-            this.code = code;
-            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
-            getStyleClass().add("special-key");
-            setOnAction(actionHandler);
-        }
-    }
-
-    private class ShiftKey extends Key {
-        EventHandler<ActionEvent> actionHandler = new EventHandler<ActionEvent>() {
-            long lastTime = -1L;
-
-            @Override public void handle(ActionEvent e) {
-                showSecondaryVK(null);
-                long time = System.currentTimeMillis();
-                if (lastTime > 0L && time - lastTime < 600L) {
-                    setState(State.SHIFT_LOCK);
-                } else if (state == State.SHIFTED || state == State.SHIFT_LOCK) {
-                    setState(State.NORMAL);
-                } else {
-                    setState(State.SHIFTED);
-                }
-                lastTime = time;
-            }
-        };
-
-        ShiftKey(double width) {
-            super("\u21d1", width);
-            setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
-            getStyleClass().add("special-key");
-            setFocusTraversable(false);
-            setOnAction(actionHandler);
-        }
-
-        private void setPressState(boolean pressed) {
-            setPressed(pressed);
-        }
-    }
-
-    private class SymbolKey extends Key {
-        String str;
-        String[] chars;
-
-        EventHandler<ActionEvent> actionHandler = new EventHandler<ActionEvent>() {
-            @Override public void handle(ActionEvent e) {
-                setState((state == State.NUMERIC) ? State.NORMAL : State.NUMERIC);
-                showSecondaryVK(null);
-            }
-        };
-
-        SymbolKey(String str, double width) {
-            super(null, width);
-            this.str = str;
-            getStyleClass().add("special-key");
-
-            if (str.length() == 1) {
-                chars = new String[] { str };
-            } else {
-                chars = str.split(" ");
-            }
-            setText(chars[0]);
-
-            setOnAction(actionHandler);
-        }
-    }
-
-    @Override public void layoutChildren(final double x, final double y,
-            final double w, final double h) {
-        double kw, kh;
-        Insets insets = getInsets();
-        if (vbox == null) {
-            createKeys();
-        }
-        HBox hbox = (HBox)vbox.getChildren().get(0);
-        double hGap = hbox.getSpacing();
-
-        double maxWidth = 0;
-        int maxNKeys = 0;
-        for (Node vnode : vbox.getChildren()) {
-            hbox = (HBox)vnode;
-            int nKeys = 0;
-            double totalWidth = 0;
-            for (Node hnode : hbox.getChildren()) {
-                Key key = (Key)hnode;
-                nKeys++;
-                totalWidth += key.keyWidth;
-            }
-
-            maxNKeys = Math.max(maxNKeys, nKeys);
-            maxWidth = Math.max(maxWidth, totalWidth);
-        }
-
-
-        if (fxvk == secondaryVK) {
-            kw = PREF_PORTRAIT_KEY_WIDTH;
-            kh = ((FXVKSkin)primaryVK.getSkin()).keyHeight;
-        } else {
-            kw = (hbox.getWidth() - (maxNKeys - 1) * hGap) / Math.max(maxWidth, 10.0);
-            kh = (getHeight() - insets.getTop() - insets.getBottom() - (keyRows.length - 1) * vbox.getSpacing()) / keyRows.length;
-        }
-
-        if (keyWidth != kw || keyHeight != kh) {
-            keyWidth = kw;
-            keyHeight = kh;
-            createKeys();
-        }
-
-        super.layoutChildren(x, y, w, h);
-
-        for (Node vnode : vbox.getChildren()) {
-            hbox = (HBox)vnode;
-            int nKeys = 0;
-            int nSpecialKeys = 0;
-            double totalWidth = 0;
-            for (Node hnode : hbox.getChildren()) {
-                Key key = (Key)hnode;
-                nKeys++;
-                if (key.keyWidth > 1.0) {
-                    nSpecialKeys++;
-                }
-                totalWidth += key.keyWidth;
-            }
-
-            double slop = hbox.getWidth() - (nKeys - 1) * hGap - totalWidth * kw;
-            for (Node hnode : hbox.getChildren()) {
-                Key key = (Key)hnode;
-                // Add slop if not landscape numerical keyboard. (Better if specified in props).
-                if ((fxvk.vkType != 1 || fxvk.getStyleClass().contains("fxvk-portrait")) &&
-                    slop > 0 && key.keyWidth > 1.0) {
-
-                    key.setPrefWidth(key.keyWidth * keyWidth + slop / nSpecialKeys);
-                } else {
-                    key.setPrefWidth(key.keyWidth * keyWidth);
-                }
+                this.chars = letterChars;
+                text.setText(this.chars);
             }
         }
     }
 
-    private void showSecondaryVK(final CharKey key) {
-        if (key != null) {
-            primaryVK = fxvk;
-            final Node textInput = primaryVK.getAttachedNode();
+    /**
+     * 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 (secondaryVK == null) {
-                secondaryVK = new FXVK();
-                secondaryVK.getStyleClass().addAll("fxvk-secondary", "fxvk-portrait");
-                secondaryVK.setSkin(new FXVKSkin(secondaryVK));
-                secondaryPopup = new Popup();
-                secondaryPopup.setAutoHide(true);
-                secondaryPopup.getContent().add(secondaryVK);
-                // TODO: call to impl_processCSS is probably not needed here
-                secondaryVK.impl_processCSS(false);
-            }
+    /**
+     * 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;
 
-            if (state == State.NUMERIC) {
-                ArrayList<String> symbols = new ArrayList<String>();
-                for (String ch : key.chars) {
-                    if (!Character.isLetter(ch.charAt(0))) {
-                        symbols.add(ch);
-                    }
+        private KeyCodeKey(String letter, String c, KeyCode code) {
+            super(letter, c);
+            this.code = code;
+        }
+
+        protected KeyEvent event(EventType<KeyEvent> type) {
+            if (type == KeyEvent.KEY_PRESSED || type == KeyEvent.KEY_RELEASED) {
+                try {
+                    Field fld = FXRobotHelper.class.getDeclaredField("inputAccessor");
+                    fld.setAccessible(true);
+                    FXRobotInputAccessor inputAccessor = (FXRobotInputAccessor)fld.get(null);
+
+                    return inputAccessor.createKeyEvent(type, code, chars, chars,
+                                          shiftDown, false, false, false);
+                } catch (Exception e) {
+                    System.err.println(e);
                 }
-                secondaryVK.chars = symbols.toArray(new String[symbols.size()]);
+                return null;
             } else {
-                ArrayList<String> secondaryChars = new ArrayList<String>();
-                // Add all letters
-                for (String ch : key.chars) {
-                    if (Character.isLetter(ch.charAt(0))) {
-                        if (state == State.SHIFTED || state == State.SHIFT_LOCK) {
-                            secondaryChars.add(ch.toUpperCase());
-                        } else {
-                            secondaryChars.add(ch);
-                        }
-                    }
-                }
-                // Add secondary character if not a letter
-                if (key.chars.length > 1 &&
-                    !Character.isLetter(key.chars[1].charAt(0))) {
-                    secondaryChars.add(key.chars[1]);
-                }
-                secondaryVK.chars = secondaryChars.toArray(new String[secondaryChars.size()]);
-            }
-
-            if (secondaryVK.chars.length > 1) {
-                if (secondaryVK.getSkin() != null) {
-                    ((FXVKSkin)secondaryVK.getSkin()).createKeys();
-                }
-
-                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);
-                HBox hbox = (HBox)vbox.getChildren().get(0);
-                final double w = insets.getLeft() + insets.getRight() +
-                                 nKeysPerRow * PREF_PORTRAIT_KEY_WIDTH + (nKeysPerRow - 1) * hbox.getSpacing();
-                final double h = insets.getTop() + insets.getBottom() +
-                                 nRows * primarySkin.keyHeight + (nRows-1) * vbox.getSpacing();
-                secondaryVK.setPrefWidth(w);
-                secondaryVK.setMinWidth(USE_PREF_SIZE);
-                secondaryVK.setPrefHeight(h);
-                secondaryVK.setMinHeight(USE_PREF_SIZE);
-                Platform.runLater(new Runnable() {
-                    public void run() {
-                        // Position popup on screen
-                        Point2D nodePoint =
-                            com.sun.javafx.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 {
-            if (secondaryVK != null) {
-                secondaryVK.setAttachedNode(null);
-                secondaryPopup.hide();
+                return super.event(type);
             }
         }
     }
 
+    /**
+     * 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 KeyboardStateKey(String t) {
+            text.setText(t);
+            getStyleClass().add("special");
+        }
+    }
 
-    static class RootWrapper extends Pane {
-        Group vkGroup;
-        double dragStartY;
+    /**
+     * A special type of KeyboardStateKey used for switching from the current
+     * virtual keyboard layout to a new one.
+     */
+    private final class SwitchBoardKey extends KeyboardStateKey {
+        private FXVK.Type type;
 
-        RootWrapper(final Region oldRoot) {
-            getChildren().add(oldRoot);
-            getChildren().add(vkGroup = new Group());
-            prefWidthProperty().bind(oldRoot.prefWidthProperty());
-            prefHeightProperty().bind(oldRoot.prefHeightProperty());
-
-
-            addEventHandler(MOUSE_PRESSED, new EventHandler<MouseEvent>() {
-                @Override public void handle(MouseEvent e) {
-                    dragStartY = e.getY() - oldRoot.getTranslateY();
-                    e.consume();
-                }
-            });
-
-            addEventHandler(MOUSE_DRAGGED, new EventHandler<MouseEvent>() {
-                @Override public void handle(MouseEvent e) {
-                    if (vkGroup.isVisible()) {
-                        double y =
-                            Math.min(0, Math.max(e.getY() - dragStartY,
-                                                 vkGroup.getLayoutY() - oldRoot.getHeight()));
-                        oldRoot.setTranslateY(y);
-                    }
-                    e.consume();
-                }
-            });
+        private SwitchBoardKey(String displayName, FXVK.Type type) {
+            super(displayName);
+            this.type = type;
         }
 
-        @Override protected double computePrefWidth(double height) {
-            return oldRoot.prefWidth(height);
+        @Override protected void release() {
+            super.release();
+            getSkinnable().setType(type);
         }
+    }
 
-        @Override protected double computePrefHeight(double width) {
-            return oldRoot.prefHeight(width);
-        }
+    private List<List<Key>> loadBoard(String boardName) {
+        try {
+            List<List<Key>> rows = new ArrayList<List<Key>>(5);
+            List<Key> keys = new ArrayList<Key>(20);
 
-        private void updateTimelines() {
-            double rootHeight = getHeight();
-            double vkHeight = vkGroup.getLayoutBounds().getHeight();
+            InputStream asciiBoardFile = FXVKSkin.class.getResourceAsStream(boardName + ".txt");
+            BufferedReader reader = new BufferedReader(new InputStreamReader(asciiBoardFile));
+            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
+            String chars = "";
+            String alt = null;
 
-            slideInTimeline.getKeyFrames().setAll(
-                new KeyFrame(Duration.ZERO,
-                             new KeyValue(vkGroup.visibleProperty(), true),
-                             new KeyValue(vkGroup.layoutYProperty(), rootHeight)),
-                new KeyFrame(Duration.millis(VK_SLIDE_MILLIS),
-                             new KeyValue(vkGroup.visibleProperty(), true),
-                             new KeyValue(vkGroup.layoutYProperty(),
-                                          Math.floor(rootHeight - vkHeight),
-                                          Interpolator.EASE_BOTH)));
+            while ((line = reader.readLine()) != null) {
+                // A single line represents a single row of buttons
+                for (int i=0; i<line.length(); i++) {
+                    char ch = line.charAt(i);
 
-            slideOutTimeline.getKeyFrames().setAll(
-                new KeyFrame(Duration.ZERO,
-                             new KeyValue(vkGroup.layoutYProperty(),
-                                          Math.floor(rootHeight - vkHeight))),
-                new KeyFrame(Duration.millis(VK_SLIDE_MILLIS),
-                             new KeyValue(vkGroup.layoutYProperty(),
-                                          rootHeight,
-                                          Interpolator.EASE_BOTH),
-                             new KeyValue(vkGroup.visibleProperty(), false)));
-        }
+                    // Process the char
+                    if (ch == ' ') {
+                        c++;
+                    } else if (ch == '[') {
+                        // Start of a key
+                        col = c;
+                        chars = "";
+                        alt = null;
+                        identifier = false;
+                    } else if (ch == ']') {
+                        // End of a key
+                        colSpan = c - col;
+                        Key key;
+                        if (identifier) {
+                            if ("$shift".equals(chars)) {
+                                key = new KeyboardStateKey("shift") {
+                                    @Override protected void release() {
+                                        pressShift();
+                                    }
+                                };
+                                key.getStyleClass().add("shift");
+                            } else if ("$backspace".equals(chars)) {
+                                key = new KeyCodeKey("backspace", "\b", KeyCode.BACK_SPACE);
+                                key.getStyleClass().add("backspace");
 
-        @Override public void layoutChildren() {
-            if (getScene() != null && getScene().getWindow() != null &&
-                getWidth() > getScene().getWindow().getWidth()) {
-                // Too soon to layout keyboard
-                return;
-            }
-
-            final double rootWidth = getWidth();
-            final double rootHeight = getHeight();
-            final double vkHeight = (rootWidth > rootHeight) ? VK_HEIGHT : VK_PORTRAIT_HEIGHT;
-
-            boolean attached = false;
-            boolean resized = false;
-            for (Node child : vkGroup.getChildren()) {
-                if (child instanceof FXVK && !child.getStyleClass().contains("fxvk-secondary")) {
-                    final FXVK fxvk = (FXVK)child;
-                    attached = (attached || fxvk.getAttachedNode() != null);
-                    if (rootWidth > rootHeight) {
-                        fxvk.getStyleClass().remove("fxvk-portrait");
-                    } else {
-                        if (!fxvk.getStyleClass().contains("fxvk-portrait")) {
-                            fxvk.getStyleClass().add("fxvk-portrait");
-                        }
-                    }
-
-                    if (fxvk.getWidth() != rootWidth || fxvk.getHeight() != vkHeight) {
-                        slideInTimeline.stop();
-                        slideOutTimeline.stop();
-                        fxvk.setPrefWidth(rootWidth);
-                        fxvk.setPrefHeight(vkHeight);
-                        vkGroup.setLayoutY(attached ? (rootHeight - vkHeight) : rootHeight);
-                        resized = true;
-                    }
-
-                    if (vkGroup.isVisible()) {
-                        Platform.runLater(new Runnable() {
-                            public void run() {
-                                Node attachedNode = fxvk.getAttachedNode();
-                                if (attachedNode != null) {
-                                    double oldRootY = oldRoot.getTranslateY();
-                                    double nodeBottom =
-                                        attachedNode.localToScene(attachedNode.getBoundsInLocal()).getMaxY() + 2;
-                                    if (nodeBottom > rootHeight - vkHeight) {
-                                        translatePane(oldRoot, rootHeight - vkHeight - nodeBottom + oldRootY);
+                            } 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 ("$caps".equals(chars)) {
+                                key = new KeyboardStateKey("caps lock") {
+                                    @Override protected void release() {
+                                        pressCaps();
                                     }
+                                };
+                                key.getStyleClass().add("caps");
+                            } else if ("$space".equals(chars)) {
+                                key = new LetterKey(" ");
+                            } 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") {
+                                    @Override protected void release() {
+                                        slideInTimeline.stop();
+                                        if (!inScene) {
+                                            winY.set(vkPopup.getY());
+                                        }
+                                        slideOutTimeline.playFromStart();
+                                    }
+                                };
+                                key.getStyleClass().add("hide");
+                            } else if ("$undo".equals(chars)) {
+                                key = new SuperKey("undo", "");
+                            } else if ("$redo".equals(chars)) {
+                                key = new SuperKey("redo", "");
+                            } else {
+                                // The name is the name of a board to show
+                                String name = chars.substring(1);
+                                if (name.equals("AsciiBoard")) {
+                                    key = new SwitchBoardKey("ABC", FXVK.Type.TEXT);
+                                } else if (name.equals("EmailBoard")) {
+                                    key = new SwitchBoardKey("ABC.com", FXVK.Type.EMAIL);
+                                } else if (name.equals("SymbolBoard")) {
+                                    key = new SwitchBoardKey("#+=", FXVK.Type.NUMERIC);
+                                } else {
+                                    throw new AssertionError("Unknown keyboard '" + name + "'");
                                 }
                             }
-                        });
+                        } else {
+                            boolean isLetter = false;
+                            try {
+                                KeyCode code = KeyCode.getKeyCode(chars.toUpperCase());
+                                isLetter = code == null ? false : code.isLetterKey();
+                            } catch (Exception e) { }
+                            key = isLetter ? new LetterKey(chars) : new SymbolKey(chars, alt);
+                        }
+                        key.col = col;
+                        key.colSpan = colSpan;
+                        if (rows.isEmpty()) {
+                            key.getStyleClass().add("short");
+                        }
+                        for (String sc : key.getStyleClass()) {
+                            key.text.getStyleClass().add(sc + "-text");
+                            key.icon.getStyleClass().add(sc + "-icon");
+                        }
+                        keys.add(key);
+                    } else {
+                        // Normal textual characters. Read all the way up to the
+                        // next ] or space
+                        for (int j=i; j<line.length(); j++) {
+                            char c2 = line.charAt(j);
+                            boolean e = false;
+                            if (c2 == '\\') {
+                                j++;
+                                i++;
+                                e = true;
+                                c2 = line.charAt(j);
+                            }
+
+                            if (c2 == '$' && !e) {
+                                identifier = true;
+                            }
+
+                            if (c2 == '|' && !e) {
+                                chars = line.substring(i, j);
+                                i = j + 1;
+                            } else if ((c2 == ']' || c2 == ' ') && !e) {
+                                if (chars.isEmpty()) {
+                                    chars = line.substring(i, j);
+                                } else {
+                                    alt = line.substring(i, j);
+                                }
+                                i = j-1;
+                                break;
+                            }
+                        }
+                        c++;
                     }
                 }
+
+                c = 0;
+                col = 0;
+                rows.add(keys);
+                keys = new ArrayList<Key>(20);
             }
-            if (vkGroup.getLayoutY() == 0) {
-                vkGroup.setLayoutY(rootHeight);
-            }
+            return rows;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return Collections.emptyList();
         }
     }
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/SymbolBoard.txt	Wed Jan 09 18:22:50 2013 +0200
@@ -0,0 +1,5 @@
+[~|` ][!|1 ][@|2 ][#|3 ][\$|4 ][%|5 ][^|6 ][&|7 ][*|8 ][(|9 ][)|0 ][_|- ][+|= ][$backspace  ]
+[$tab  ][q ][w ][e ][r ][t ][y ][u ][i ][o ][p ][{|\[ ][}|\] ][\||\\ ]
+[$caps   ][a ][s ][d ][f ][g ][h ][j ][k ][l ][:|; ]["|' ][$enter  ]
+[$shift    ][z ][x ][c ][v ][b ][n ][m ][<|, ][>|. ][?|/ ][$shift   ]
+[$hide   ][$AsciiBoard  ][$space               ][$AsciiBoard  ][$hide  ]
\ No newline at end of file
--- a/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/embedded.css	Wed Jan 09 15:29:55 2013 +0200
+++ b/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/embedded.css	Wed Jan 09 18:22:50 2013 +0200
@@ -139,109 +139,6 @@
 }
 
 
-
-/*******************************************************************************
- *                                                                             *
- * Virtual Keyboard                                                            *
- *                                                                             *
- ******************************************************************************/
-.fxvk {
-    -fx-cursor: default;
-    -fx-background-color: #a1a1a1, #d7d7d7, #c2c2c2;
-    -fx-background-insets: 0, 1 0 0 0, 2 0 0 0;
-    -fx-padding: 8 4 10 4;
-}
-
-.fxvk #vbox {
-    -fx-spacing: 8;
-}
-
-.fxvk #hbox {
-    -fx-spacing: 8;
-}
-
-.fxvk-secondary {
-    -fx-background-color: #ffffff, #ededed;
-    -fx-background-insets: 0, 1;
-    -fx-background-radius: 8;
-    -fx-padding: 10;
-    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.75), 8, 0.0, 0, 0);
-}
-
-.fxvk .key { 
-    -fx-background-color: #acacac,
-                          linear-gradient(to bottom, #f8f8f8, #acacac),
-                          linear-gradient(to bottom, #dedede, #acacac);
-    -fx-background-insets: 0, 1, 2;
-    -fx-font-weight: bold;
-    -fx-font: bold 36px "Amble";
-    -fx-text-fill: #333333;
-    -fx-padding: 3 3 5 0;
-    -fx-background-radius: 4;
-    -fx-effect: dropshadow(three-pass-box, rgba(0, 0, 0, 0.75), 2, 0.0, 0, 1);
-}
-
-.fxvk-portrait .key { 
-    -fx-padding: 3 0 5 0;
-}
-
-.fxvk .key:pressed { 
-    -fx-background-color: #464646,
-                          linear-gradient(to bottom, #e8e8e8, #9c9c9c),
-                          linear-gradient(to bottom, #c3c3c3, #888888);
-}
-
-.fxvk .multi-char-key { 
-    -fx-font: bold 20px "Amble";
-}
-
-.fxvk .key .label { 
-    -fx-text-fill: #959595;
-    -fx-font: bold 14px "Amble";
-    -fx-alignment: TOP_RIGHT;
-}
-
-.fxvk .multi-char-key .label { 
-    -fx-font: bold 12px "Amble";
-}
-
-.fxvk .special-key { 
-    -fx-background-color: linear-gradient(to bottom, #676767, #404040),
-                          linear-gradient(to bottom, #808080, #3e3e3e),
-                          linear-gradient(to bottom, #686868, #3e3e3e);
-    -fx-background-insets: 0, 1, 2;
-    -fx-font: bold 22px "Amble";
-    -fx-text-fill: white;
-    -fx-padding: 3 0 5 0;
-}
-
-.fxvk .special-key:disabled { 
-    -fx-background-color: #707070;
-}
-
-/* The url based images don't load properly. Using workaround in FXVKSkin instead.
-.fxvk #shift-key {
-    -fx-graphic: url("fxvk-shift-button.png");
-}
-
-.fxvk #capslock-key {
-    -fx-graphic: url("fxvk-capslock-button.png");
-}
-
-.fxvk #shift-key:disabled {
-    -fx-graphic: null;
-}
-
-.fxvk #backspace-key {
-    -fx-graphic: url("fxvk-backspace-button.png");
-}
-
-.fxvk #enter-key {
-    -fx-graphic: url("fxvk-enter-button.png");
-}
-*/
-
-
 /*******************************************************************************
  *
  * 2-level focus setting.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/fxvk.css	Wed Jan 09 18:22:50 2013 +0200
@@ -0,0 +1,85 @@
+/*******************************************************************************
+ *                                                                             *
+ * Virtual Keyboard                                                            *
+ *                                                                             *
+ ******************************************************************************/
+
+.fxvk {
+    -fx-cursor: default;
+    -fx-background-color: linear-gradient(to bottom, rgb(126, 126, 126) 0%, rgb(76, 76, 76) 10%, rgb(84, 84, 84) 100%);
+    -fx-background-insets: 0;
+    -fx-padding: 8 4 10 4;
+}
+
+.backspace-icon {
+    -fx-background-image: url("images/backspace-icon.png");
+    -fx-background-repeat: no-repeat;
+    -fx-background-position: center;
+}
+
+.enter-icon {
+    -fx-background-image: url("images/enter-icon.png");
+    -fx-background-repeat: no-repeat;
+    -fx-background-position: center;
+}
+
+.shift-icon {
+    -fx-background-image: url("images/shift-icon.png");
+    -fx-background-repeat: no-repeat;
+    -fx-background-position: center;
+}
+
+.hide-icon {
+    -fx-background-image: url("images/vk-hide.png");
+    -fx-background-repeat: no-repeat;
+    -fx-background-position: center;
+}
+
+.key {
+    -fx-border-image-source: url("images/vk-light.png");
+    -fx-border-image-slice: 14 fill;
+    -fx-border-image-width: 14;
+    -fx-border-image-repeat: stretch;
+    -fx-border-image-insets: -3 -3 -3 -3;
+    -fx-padding: 3 3 5 0;
+}
+
+.key:pressed {
+    -fx-border-image-source: url("images/vk-light-pressed.png");
+    -fx-padding: 5 3 3 0;
+}
+
+.key-text {
+    -fx-font-size: 22px;
+    -fx-fill: #333333;
+}
+
+.key.special {
+    -fx-border-image-source: url("images/vk-dark.png");
+}
+
+.key.special:pressed {
+    -fx-border-image-source: url("images/vk-dark-pressed.png");
+    -fx-padding: 5 3 3 0;
+}
+
+.special-text {
+    -fx-font-size: 18px;
+    -fx-fill: rgb(230, 230, 230);
+}
+
+.key.short {
+    -fx-border-image-source: url("images/vk-medium.png");
+}
+
+.key.short:pressed {
+    -fx-border-image-source: url("images/vk-medium-pressed.png");
+    -fx-padding: 5 3 3 0;
+}
+
+.short .key-text {
+    -fx-font-size: 20px;
+    -fx-font-weight: normal;
+    -fx-fill: #333333;
+}
+
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/backspace-icon.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/capslock-icon.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/enter-icon.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/shift-icon.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-dark-pressed.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-dark.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-hide.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-light-pressed.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-light.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-medium-pressed.png has changed
Binary file javafx-ui-controls/src/com/sun/javafx/scene/control/skin/caspian/images/vk-medium.png has changed