Monday, January 19, 2009

Flex Onscreen Keyboard

Upadte- 8/18/2009 9:31 AM
Added Sample.
Added a fix for the enter key in text areas and for editable DateFields.

Upadte- 1/20/2009 2:49 PM
Added logic to show or hide the keyboard automatically based on whether a input box is selected. This can be overridden by setting auto to false. Also clicking show/hide will toggle this feature.


This component adds a simple Onscreen Keyboard in flex. The keyboard is very customizable built at run time from a provided array.

It supports buttons to insert text as well as the special actions listed below and custom events and methods.

Button Types:
SHIFT Toggle the shift state.
TAB Tab to the next control on screen, or previous if shift is down.
ENTER Simulate an Enter action, causes validation/submit action.
HIDE Hide the onscreen keyboard.
BACKSPACE Delete the selected text or one character backward, or forward on shift.
MODE Switch to the next keyboard layout, or previous on shift.
Sample:
private const sampleKeyMap:Object = [[
{key: 'a', altkey: 'A'},
{key: 'b', altkey: 'B'},
{key: 'c', altkey: 'C'},
{key: ' ', label: 'Space', span: 2}
], [
{key: '', label: 'Backspace', altlabel: 'Delete', span: 3, type: OnscreenKeyboard.BACKSPACE}
{label: 'Shift', span: 2, type: OnscreenKeyboard.SHIFT},
{label: 'myFucntion', method: myFunction},
{label: 'myEvent', event: 'myEvent'}
]];

private function myFunction(event:MouseEvent):void
{
Alert.Show(osk.shift ? "Shift DOwn" : "Shift Up");
}

<mx:TextInput/>
<local.OnscreenKeyboard id="osk" modes="{sampleKeyMap}" myEvent="Alert.Show('I got clicked.')"/>
OnscreenKeyboard.as
package
{
import flash.events.Event;
import flash.events.IEventDispatcher;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.ui.Keyboard;

import mx.binding.utils.BindingUtils;
import mx.binding.utils.ChangeWatcher;
import mx.containers.HBox;
import mx.containers.TitleWindow;
import mx.containers.VBox;
import mx.controls.TextArea;
import mx.controls.TextInput;
import mx.events.FlexEvent;
import mx.events.FocusRequestDirection;
import mx.events.PropertyChangeEvent;

/**
* This is a suctomizable onscreen keyboard. You can use the provided keymaps of provide your own.
*
* A keymap consists of a two dimensional array of keys. The outer array is a collection of rows and the inner array's are
* the keys. Each key is an object with any of the following properties.
*
* keyMap:Object {
* key: Characters to insert on key press.
* altkey: Alternate characters to insert on shift click.
* label: Normal label to display.
* altlabel: Alternate label to display on shift.
* span: Number of button spaces to span over.
* method: Custom method to raise on click. (Can access OnscreenKeyboard.shift to detect shift state.)
* event: Custom event to raise on click. (Can access OnscreenKeyboard.shift to detect shift state.)
* type: Special type of internal action to preform from the following:
* SHIFT Toggle the shift state.
* TAB Tab to the next control on screen, or previous if shift is down.
* ENTER Simulate an Enter action, causes validation/submit action.
* HIDE Hide the onscreen keyboard.
* BACKSPACE Delete the selected text or one character backward, or forward on shift.
* MODE Switch to the next keyboard layout, or previous on shift.
*
*
* Example:
* private const sampleKeyMap:Object = [[
* {key: 'a', altkey: 'A'},
* {key: 'b', altkey: 'B'},
* {key: 'c', altkey: 'C'},
* {key: ' ', label: 'Space', span: 2}
* ], [
* {key: '', label: 'Backspace', altlabel: 'Delete', span: 3, type: OnscreenKeyboard.BACKSPACE}
* {label: 'Shift', span: 2, type: OnscreenKeyboard.SHIFT},
* {label: 'myFucntion', method: myFunction},
* {label: 'myEvent', event: 'myEvent'}
* ]];
*
* @author Jeremy Pyne jeremy.pyne@gmail.com
*/
public class OnscreenKeyboard extends TitleWindow
{
/**
* Create a new instance of OnscreenKeyboard.
*/
public function OnscreenKeyboard()
{
super();

// Set up panel spaceing.
setStyle("paddingBottom", 6);
setStyle("paddingLeft", 6);
setStyle("paddingRight", 6);

// Set default modes.
modes = [alphaKeyMap, qwertyKeyMap];

// Listen for creation complete event.
addEventListener(FlexEvent.CREATION_COMPLETE, onInit);
}

/**
* Width of a one span button. A two span button would be 2x keyWidth + keyGap.
*/
public var keyWidth:Number = 40;

/**
* Horizontal gap between buttons.
*/
public var keyGap:Number = 8;

/**
* Auto show/hide the keyboard.
*/
public var auto:Boolean = true;

/**
* Keyboard visibility state.
*/
private var _visible:Boolean = false;

[Bindable("visibleChanged")]
/**
* Is the keyboard currently visible.
*/
public function get isVisible():Boolean
{
return _visible;
}
public function set isVisible(value:Boolean):void
{
_visible = value;

dispatchEvent(new Event("visibleChanged"));
}

[Bindable("visibleChanged")]
/**
* Is the keyboard currently hidden?
*/
public function get notVisible():Boolean
{
return !_visible;
}
public function set notVisible(value:Boolean):void
{
_visible = !value;

dispatchEvent(new Event("visibleChanged"));
}

[Bindable]
/**
* Is the shift button pressed?
*/
public var shift:Boolean = false;

/**
* Next keyboard to load from modes.
*/
private var nextMode:Number = -1;

/**
* Array of keyboard layouts. This can be changed to use cutsom layouts.
*/
public var modes:Array = new Array();

/**
* Rendered keymaps for displaying onscreen and storing when switched out.
*/
private var keyMaps:Array = new Array();

/**
* Keyboard portion of the panel.
*/
private var showView:VBox;

/**
* Hidden version of the keyboard with a show button.
*/
private var hideView:HBox;

/**
* Shift button for toggleing the shift state.
*/
public static const SHIFT:String = "shift";
/**
* Tab button to go to the next control.
*/
public static const TAB:String = "tab";
/**
* Enter button to validate and submit a form.
*/
public static const ENTER:String = "enter";
/**
* Hide button to hide the keyboard.
*/
public static const HIDE:String = "hide";
/**
* Backspace button to delete the selected text or a charecter.
*/
public static const BACKSPACE:String = "backspace";
/**
* Mode button to switch keyboard layouts.
*/
public static const MODE:String = "mode";

/**
* Run on creation compleate and load the default keymap.
*/
private function onInit(event:FlexEvent):void
{
// Load the default keymap.
loadKeyMap();

// Listen for focus change event.
parentApplication.addEventListener(FocusEvent.FOCUS_IN, focusIn);
parentApplication.addEventListener(FocusEvent.FOCUS_OUT, focusOut);

// Set the initial keyboard state.
isVisible = focusManager.getFocus() is mx.controls.TextInput || focusManager.getFocus() is mx.controls.TextArea;
}

/**
* Raised when a object gains focus.
*/
private function focusIn(event:FocusEvent):void
{
// If auto is on:
if(auto) {
// If a textbox is selected enable the keyobard.
if(event.target is UITextField)
isVisible = true;
// If not then disable the keyobard.
else
isVisible = false;
}
}

/**
* Raised when a object loses focus.
*/
private function focusOut(event:FocusEvent):void
{
// If auto is on hide the keyboard.
if(auto)
isVisible = false;
}

/**
* Activate a new keymap. If it hasn't been loaded yet load it.
*/
private function loadKeyMap():void
{
if(!shift) {
// Switch to the next mode.
nextMode = nextMode + 1 == modes.length ? 0 : nextMode + 1
} else {
// Switch to the prevoise mode.
nextMode = nextMode == 0 ? modes.length - 1 : nextMode - 1
}

// If this modes keymap isnt loaded, build it.
if(!keyMaps[nextMode])
{
// Prepare the new keymap.
keyMaps[nextMode] = new Array();

// For each row of keys.
for each (var rowMap:Object in modes[nextMode])
{
// Create a new row.
var row:HBox = new HBox();
// Set a custom horazontal gap.
row.setStyle("horizontalGap", keyGap);

// For each key in the row.
for each (var keyData:Object in rowMap)
{
// Create a new key.
var key:Button = new Button();
// Listen for the key press and call key_clickHandler or a custom method.
key.addEventListener(MouseEvent.CLICK, keyData.event ? keyData.event : key_clickHandler);

/**
* This extra property and shit variable is created so that all keys will automaticly detect shift changes and update
* their labels without having to loop through them manualy.
*/
// Bind this keys shift state to the panel.
BindingUtils.bindProperty(key, "shift", this, "shift");
// Watch for shift changes and fire off key_shiftListener to udate the keys label.
ChangeWatcher.watch(key, "shift", key_shiftListener);

// Set the initial label to an unshifted state.
key.label = keyData.label ? keyData.label : keyData.key;

// Set the buttons width to (span * keyWidth) + (keyGap * keyWidth - 1).
key.width = keyData.span? keyData.span * (keyWidth + keyGap) - keyGap : keyWidth;

// If this button is a toggle button, set toggle.
key.toggle = keyData.toggle;

// Store all the data for this key in it's data field for later use.
key.data = keyData;

// Disable focus on this key so that it won't steal focus.
key.focusEnabled = false;

// If this is a special shift key.
if(keyData.type == OnscreenKeyboard.SHIFT) {
// Bind the selected field to the shift state.
BindingUtils.bindProperty(key, "selected", this, "shift");
// Force the key to a toggle key.
key.toggle = true;
}

// Add this eky to the row.
row.addChild(key);
}

// Add the row to the key map.
keyMaps[nextMode].push(row);
}
}

// Clear the old rows of keys.
showView.removeAllChildren();

// oop through each row of keys.
for each(var rowBox:HBox in keyMaps[nextMode])
// Add this row to the keyboard view.
showView.addChild(rowBox);
}

/**
* Override the createChildren method
*/
override protected function createChildren():void
{
super.createChildren();

// Prepare the main keyboard view.
if(!showView) {
// Create a VBox for the rows.
showView = new VBox();

// Bind the visibility to the isVisible.
BindingUtils.bindProperty(showView, "visible", this, "isVisible");
BindingUtils.bindProperty(showView, "includeInLayout", this, "isVisible");

// Add the main view to the panel.
this.addChild(showView);
}

// Prepare the collapsed view.
if(!hideView) {
// Create a HBox for teh collapsed buttons.
hideView = new HBox();

// Bind the visibility to the notVisible.
BindingUtils.bindProperty(hideView, "visible", this, "notVisible");
BindingUtils.bindProperty(hideView, "includeInLayout", this, "notVisible");

// Create a show button.
var show:Button = new Button()

// Set the button label.
show.label = "Show Keyboard";

// Disable the focus for the button so it doesn't steal focus.
show.focusEnabled = false;

// Listen for the show button to be clicked.
show.addEventListener(MouseEvent.CLICK, show_clickHandler);

// Add the show button to the HBox.
hideView.addChild(show);

// Add the collapsed view to the panel.
this.addChild(hideView);
}
}

/**
* Fires then the show button is clicked.
*/
private function show_clickHandler(event:MouseEvent):void
{
// Set the isVisible to true.
isVisible = true;
// Toggle the autohide mode.
auto = !auto;
}

/**
* Fires when a key is perssed unless said key has a custom key handler.
*/
private function key_clickHandler(event:MouseEvent):void
{
// Custom shift action.
if(event.currentTarget.data.type == OnscreenKeyboard.SHIFT) {
// Toggle shift state.
shift = !shift;
return;
}
// Custom hide keyboard action.
if(event.currentTarget.data.type == OnscreenKeyboard.HIDE) {
// Switch shift off and hide the keyboard.
isVisible = shift = false;
// Toggle the autohide mode.
auto = !auto;
return;
}
// Custom enter action.
if(event.currentTarget.data.type == OnscreenKeyboard.ENTER) {
if(focusManager.getFocus() is mx.controls.TextArea) {

} else {
// If there if a IEventDispatcher selected.
if(focusManager.getFocus() is IEventDispatcher) {
// Pass it a keyDown and keyUp event.
(focusManager.getFocus()as IEventDispatcher).dispatchEvent(new KeyboardEvent("keyDown", true, false, Keyboard.ENTER, Keyboard.ENTER, 0, false, false, shift));
(focusManager.getFocus()as IEventDispatcher).dispatchEvent(new KeyboardEvent("keyUp", true, false, Keyboard.ENTER, Keyboard.ENTER, 0, false, false, shift));
}

// Switch shift off.
shift = false;
return;
}
}
if(event.currentTarget.data.type == OnscreenKeyboard.TAB) {
// If there if a IEventDispatcher selected.
if(focusManager.getFocus() is IEventDispatcher) {
focusManager.moveFocus(shift ? FocusRequestDirection.BACKWARD : FocusRequestDirection.FORWARD);
}

// Switch shift off.
shift = false;
return;
}
// Custom switch mode action.
if(event.currentTarget.data.type == OnscreenKeyboard.MODE) {
// Load the next keymap.
loadKeyMap();

// Switch shift off.
shift = false;
return;
}
// Fire a custom event.
if(event.currentTarget.data.event) {
// Fire the custom event.
dispatchEvent(new Event(event.currentTarget.data.event));

// Switch shift off.
shift = false;
return;
}

// Set the key charecter to altkey if shift is set or key.
var char:String = shift && event.currentTarget.data.altkey ? event.currentTarget.data.altkey : event.currentTarget.data.key;
var stepLeft:Boolean = false;
var stepRight:Boolean = false;

// If a TextInput has focus.
if(focusManager.getFocus() is mx.controls.TextInput) {
// Get the focused control.
var tiControl:mx.controls.TextInput = (focusManager.getFocus() as mx.controls.TextInput);
stepLeft = event.currentTarget.data.type == OnscreenKeyboard.BACKSPACE && tiControl.selectionBeginIndex == tiControl.selectionEndIndex && shift == false;
stepRight = event.currentTarget.data.type == OnscreenKeyboard.BACKSPACE && tiControl.selectionBeginIndex == tiControl.selectionEndIndex && shift == true;

// Replace the selected text with the charecter. If step then replace an extra charecter.
tiControl.text = tiControl.text.substr(0, tiControl.selectionBeginIndex - (stepLeft ? 1 : 0)) + char + tiControl.text.substr(tiControl.selectionEndIndex + (stepRight ? 1 : 0), tiControl.text.length - tiControl.selectionEndIndex)
// Reposition the cursor to the right the length of the charecters inserted or adjust for a backspace or delete.
tiControl.selectionBeginIndex = tiControl.selectionEndIndex = tiControl.selectionBeginIndex + char.length - (event.currentTarget.data.type == OnscreenKeyboard.BACKSPACE ? (stepLeft ? 1 : 0) : -1);
}
// If a TextArea has focus.
if(focusManager.getFocus() is mx.controls.TextArea) {
// Get the focused control.
var taControl:mx.controls.TextArea = (focusManager.getFocus() as mx.controls.TextArea);
stepLeft = event.currentTarget.data.type == OnscreenKeyboard.BACKSPACE && taControl.selectionBeginIndex == taControl.selectionEndIndex && shift == false;
stepRight = event.currentTarget.data.type == OnscreenKeyboard.BACKSPACE && taControl.selectionBeginIndex == taControl.selectionEndIndex && shift == true;

// Replace the selected text with the charecter. If step then replace an extra charecter.
taControl.text = taControl.text.substr(0, taControl.selectionBeginIndex - (stepLeft ? 1 : 0)) + char + taControl.text.substr(taControl.selectionEndIndex + (stepRight ? 1 : 0), taControl.text.length - taControl.selectionEndIndex)
// Reposition the cursor to the right the length of the charecters inserted or adjust for a backspace or delete.
taControl.selectionBeginIndex = taControl.selectionEndIndex = taControl.selectionBeginIndex + char.length - (event.currentTarget.data.type == OnscreenKeyboard.BACKSPACE ? (stepLeft ? 1 : 0) : -1);
}
// If a DateField has focus
if(focusManager.getFocus() is mx.controls.DateField) {
// Get the focused control.
var tdControl:mx.controls.DateField = (focusManager.getFocus() as mx.controls.DateField);
if(tdControl.editable) {
if(event.currentTarget.data.type == OnscreenKeyboard.BACKSPACE)
// Can't access selected text so jsut delete last letter.
tdControl.text = tdControl.text.substr(0, tdControl.text.length - 1);
else
// Can't access selected text so just appent to text.
tdControl.text = tdControl.text + char;

// Fire off a chaneg event on the source control.
tdControl.dispatchEvent(new Event(Event.CHANGE, false, false));
}
}


// Switch shift off.
shift = false;
}

/**
* Fires when the shift state has changed on all buttons.
*/
private function key_shiftListener(event:PropertyChangeEvent):void
{
//If shift is clicked use altlabel or the altkey.
if(shift && event.source.data.altlabel)
event.source.label = event.source.data.altlabel;
else if(shift && event.source.data.altkey)
event.source.label = event.source.data.altkey;
// If neither of these exist or shift isn't down then use the label or the key.
else if(event.source.data.label)
event.source.label = event.source.data.label;
else
event.source.label = event.source.data.key;
}

/**
* Default alphabetical keyboard layout.
*/
private const alphaKeyMap:Object = [[
{key: '1', altkey: '!'},
{key: '2', altkey: '@'},
{key: '3', altkey: '#'},
{key: '4', altkey: '$'},
{key: '5', altkey: '%'},
{key: '6', altkey: '^'},
{key: '7', altkey: '&'},
{key: '8', altkey: '*'},
{key: '9', altkey: '('},
{key: '0', altkey: ')'},
{key: '-', altkey: '_'},
{key: '=', altkey: '+'},
{key: '', label: 'Backspace', altlabel: 'Delete', span: 3, type: OnscreenKeyboard.BACKSPACE}
], [
{key: 'a', altkey: 'A'},
{key: 'b', altkey: 'B'},
{key: 'c', altkey: 'C'},
{key: 'd', altkey: 'D'},
{key: 'e', altkey: 'E'},
{key: 'f', altkey: 'F'},
{key: 'g', altkey: 'G'},
{key: 'h', altkey: 'H'},
{key: 'i', altkey: 'I'},
{key: 'j', altkey: 'J'},
{key: '[', altkey: '{'},
{key: ']', altkey: '}'},
{key: '\\', altkey: '|'},
{label: 'Tab', span: 2, type: OnscreenKeyboard.TAB},
{label: 'Enter', span: 2, type: OnscreenKeyboard.ENTER}
], [
{key: 'k', altkey: 'K'},
{key: 'l', altkey: 'L'},
{key: 'm', altkey: 'M'},
{key: 'n', altkey: 'N'},
{key: 'o', altkey: 'O'},
{key: 'p', altkey: 'P'},
{key: 'q', altkey: 'Q'},
{key: 'r', altkey: 'R'},
{key: 's', altkey: 'S'},
{key: ';', altkey: ':'},
{key: "'", altkey: '"'},
{key: ' ', label: 'Space', span: 2},
{label: 'Shift', span: 2, type: OnscreenKeyboard.SHIFT}
], [
{key: 't', altkey: 'T'},
{key: 'u', altkey: 'U'},
{key: 'v', altkey: 'V'},
{key: 'w', altkey: 'W'},
{key: 'x', altkey: 'X'},
{key: 'y', altkey: 'Y'},
{key: 'z', altkey: 'Z'},
{key: ',', altkey: '<'},
{key: '.', altkey: '>'},
{key: '/', altkey: '?'},
{key: '`', altkey: '~'},
{label: 'Mode', span: 2, type: OnscreenKeyboard.MODE},
{label: 'Hide', span: 2, type: OnscreenKeyboard.HIDE}
]];

/**
* Default qwerty keyboard layout.
*/
private const qwertyKeyMap:Object = [[
{key: '1', altkey: '!'},
{key: '2', altkey: '@'},
{key: '3', altkey: '#'},
{key: '4', altkey: '$'},
{key: '5', altkey: '%'},
{key: '6', altkey: '^'},
{key: '7', altkey: '&'},
{key: '8', altkey: '*'},
{key: '9', altkey: '('},
{key: '0', altkey: ')'},
{key: '-', altkey: '_'},
{key: '=', altkey: '+'},
{key: '', label: 'Backspace', altlabel: 'Delete', span: 3, type: OnscreenKeyboard.BACKSPACE}
], [
{key: 'q', altkey: 'Q'},
{key: 'w', altkey: 'W'},
{key: 'e', altkey: 'E'},
{key: 'r', altkey: 'R'},
{key: 't', altkey: 'T'},
{key: 'y', altkey: 'Y'},
{key: 'u', altkey: 'U'},
{key: 'i', altkey: 'I'},
{key: 'o', altkey: 'O'},
{key: 'p', altkey: 'P'},
{key: '[', altkey: '{'},
{key: ']', altkey: '}'},
{key: '\\', altkey: '|'},
{label: 'Tab', span: 2, type: OnscreenKeyboard.TAB},
{label: 'Enter', span: 2, type: OnscreenKeyboard.ENTER}
], [
{key: 'a', altkey: 'A'},
{key: 's', altkey: 'S'},
{key: 'd', altkey: 'D'},
{key: 'f', altkey: 'F'},
{key: 'g', altkey: 'G'},
{key: 'h', altkey: 'H'},
{key: 'j', altkey: 'J'},
{key: 'k', altkey: 'K'},
{key: 'l', altkey: 'L'},
{key: ';', altkey: ':'},
{key: "'", altkey: '"'},
{key: ' ', label: 'Space', span: 2},
{label: 'Shift', span: 2, type: OnscreenKeyboard.SHIFT}
], [
{key: 'z', altkey: 'Z'},
{key: 'x', altkey: 'X'},
{key: 'c', altkey: 'C'},
{key: 'v', altkey: 'V'},
{key: 'b', altkey: 'B'},
{key: 'n', altkey: 'N'},
{key: 'm', altkey: 'M'},
{key: ',', altkey: '<'},
{key: '.', altkey: '>'},
{key: '/', altkey: '?'},
{key: '`', altkey: '~'},
{label: 'Mode', span: 2, type: OnscreenKeyboard.MODE},
{label: 'Hide', span: 2, type: OnscreenKeyboard.HIDE}
]];

/**
* Default number pad layout.
*/
private const numberKeyMap:Object = [[
{key: '1'},
{key: '2'},
{key: '3'},
{key: '/'}
], [
{key: '4'},
{key: '5'},
{key: '6'},
{key: '*'}
], [
{key: '7'},
{key: '8'},
{key: '9'},
{key: '-'}
], [
{key: '0', span: 2},
{key: '.'},
{key: '+'}
], [
{key: '', label: 'Backspace', altlabel: 'Delete', span: 2, type: OnscreenKeyboard.BACKSPACE},
{label: 'Enter', span: 2, type: OnscreenKeyboard.ENTER}
], [
{label: 'Mode', span: 2, type: OnscreenKeyboard.MODE},
{label: 'Hide', span: 2, type: OnscreenKeyboard.HIDE}
]];
}
}


Button.as
package
{
import mx.controls.Button;
/**
* Custom Button control with an extra shift property.
*
* @author Jeremy Pyne jeremy.pyne@gmail.com
*/
public class Button extends mx.controls.Button
{
/**
* Create an instance of the Button class.
*/
public function Button()
{
super();
}

[Bindable]
public var shift:Boolean = false;
}
}

4 comments:

Unknown said...

Hi Jeremy,

I'm very interested in this component and would like to test it out. Do you have associated .swc file or maybe .mxml? It would be greatly appreciated if you could post or send me these files to alivitz@gmail.com. Thanks!

swiftwatch said...

Thanks for sharing!!!!

john said...

Have you updated or thought about updating the 'Time Stepper' component so it will work with Sdk 4.1 or higher?

arbamz said...

Hie!!! I'm a newbie at Flex; 1 week old.

Thank you very much for this.

I wanted to find out if I can use this keyboard with an editable datagrid???