ActionScript.org Flash, Flex and ActionScript Resources - http://www.actionscript.org/resources
AS3 On Screen Keyboard
http://www.actionscript.org/resources/articles/711/1/AS3-On-Screen-Keyboard/Page1.html
Keith Sumner
I am a Senior Software Developer in Irvine, Ca. My focus lately has been in developing RIA's in flash and flex. My language of choice for this is AS3. In my free time I create models in 3d studio max and program mini applications with the directx sdk and c++.
 
By Keith Sumner
Published on January 3, 2008
 
This tutorial will walk you step by step through the creation of an Actionscript 3.0 application. Some of the topics covered in this article are: Events and Event Listeners, Classes, Arrays, and loading and using XML data.

Introduction
This tutorial is designed to introduce people to some of the new features in AS3. The application that is made throughout this tutorial is built entirely in actionscript.

Some of the topics to be covered:
  1. Events and Event Listeners
  2. Classes
  3. Loading and using XML files
  4. Arrays
  5. Loops
File structure:

CustomKeyboard.fla
CustomKeyboard.swf
actionscript/CustomKeyboard.as
actionscript/KeyButton.as
xml/keys.xml

Download the supporting source file.
Screenshot:

Note: The application requires loading xml files and I am unable to upload the file to the server. So to see the application in action please download the source zip file. The link to the zip file is located on the final page of this tutorial.

CustomKeyboard.fla file setup
The main file for our application is CustomKeyboard.fla. When you open up and view this file you will notice that is is completely empty. Or is it? The only data in this file is the size of the flash application and the location of the document class file. You can modify both of these in the properties panel.
Figure 1. Properties Panel Settings


Once those settings are setup this file is completed.

CustomKeyboard Class File
The comments in the source code are also part of the tutorial. So please read them to.
Complete source code for file CustomKeyboard.as
[as]
day
package actionscript{

    import flash.display.MovieClip;
    import flash.net.URLLoader;
    import flash.net.URLRequest;
    import flash.events.*;
    import flash.text.TextField;

    public class CustomKeyboard extends MovieClip {

        static var keyData:XML = new XML;// used to store the loaded XML data.
        static var keyHolder:MovieClip = new MovieClip;// used to hold all the buttons
        static var textScreen:TextField = new TextField;// Our textfield that we write to.
        static var shift:Boolean = false;// Tells if the shift key is down.
        static var caps:Boolean = false;// Tells if the Caps Loack key is down.

        public function CustomKeyboard() {
            buildTextScreen();
            loadKeyboardinfo();
            setupKeyHolder();
        }
        static public function capsToggle():void {
            if (caps) {
                caps = false;
            } else {
                caps = true;
            }
        }
        static public function shiftToggle():void {
            if (shift) {
                shift = false;
            } else {
                shift = true;
            }
            if (caps) {
                caps = false;
            }
        }
        private function buildTextScreen():void {
            textScreen.x = 5;// position on the main stage
            textScreen.y = 5;// position on the main stage
            textScreen.width = 640;// width of the textScreen
            textScreen.height = 190;// height of the textScreen
            textScreen.htmlText = "";//
            textScreen.wordWrap = true;// makes the textScreen wrap text to a newline once it reaches the end.
            textScreen.selectable = false;// restricts teh user from being able to select the text in textScreen
            textScreen.border = true;// draws a border around the textScreen
            addChild(textScreen);// adds the textScreen to the main stage
        }
        private function loadKeyboardinfo():void {
            var xmlLoader:URLLoader = new URLLoader;// creates a loader to load the xml data.
            var xmlRequester:URLRequest = new URLRequest("xml/keys.xml");// this request object is used to tell the loader what and where to load.
            xmlLoader.addEventListener(Event.COMPLETE,xmlLoaded);// adds an event listener that fires once the loader has completed loading the xml file.
            xmlLoader.addEventListener(IOErrorEvent.IO_ERROR,ioErrorHandler);// this event listener fires if there is a problem finding or loading the xml file.
            xmlLoader.load(xmlRequester);// starts the loading of the xml file.
        }
        private function setupKeyHolder():void {
            keyHolder.x = 20;// positions the keyHolder movieclip on the main stage.
            keyHolder.y = 200;// positions the keyHolder movieclip on the main stage.
            addChild(keyHolder);// adds the keyHolder movieclip to the main stage.
        }
        private function buildKeyboard():void {
            var numKeys:Number = getNumberOfKeys();// uses a function to find out how many keys are in the XML file.
            var xPlacement:Number = 0;// used to position the keys in the keyHolder movieclip.
            for (var i = 0; i < keyData.row.length(); i++) {// loop used to draw each row.
                for (var j = 0; j < keyData.row[i].key.length(); j++) {// loop used to draw all the keys in a row.
                    var newKey:KeyButton = new KeyButton;// creates a new instance of the KeyButton class ( our onscreen button ).
                    newKey.code = keyData.row[i].key[j].code;// sets the newKey's code variable to the appropriate XML node value for that key.
                    newKey.char = keyData.row[i].key[j].char;// sets the newKey's char variable to the appropriate XML node value for that key.
                    newKey.shiftChar = keyData.row[i].key[j].shiftChar;// sets the newKey's shiftChar variable to the appropriate XML node value for that key.
                    newKey.keyWidth = checkWidth(keyData.row[i].key[j].char);// uses a function to determine the width of the key.
                    newKey.x = xPlacement + 5;// used to position the key on the x (up and down).
                    newKey.y = (newKey.keyHeight + 5) * i;// used to position the key on the y ( left and right ).
                    newKey.build();// calls a function that is inside the newKey object. More info in KeyButton.as
                    xPlacement += newKey.keyWidth+ 5;// sets the xPlacement variable for the next pass through the loop.
                    keyHolder.addChild(newKey);// adds the newKey to the keyHolder movieclip. The keyHolder is already on the stage by now so the key will display.
                }
                xPlacement = 0;// resets the x postions once a row has been drawn so the next row starts where it is suppose to.
            }
        }
        private function checkWidth(char:String):Number {
            var mediumKeys:Array = new Array('Tab','Enter','Backspace','Caps Lock');
            var shiftKey:String = "Shift";
            var spaceKey:String = "Space Bar";
            for (var m = 0; m < mediumKeys.length; m++) {
                if (char == mediumKeys[m]) {
                    return 75;
                }
            }
            if (char == shiftKey) {
                return 95;
            }
            if (char == spaceKey) {
                return keyHolder.width;
            }
            return 35;
        }
        private function getNumberOfKeys():Number {
            var keyCounter:Number = 0;
            for (var i = 0; i < keyData.row.length(); i++) {
                keyCounter += keyData.row[i].key.length();
            }
            return keyCounter;
        }
        private function ioErrorHandler(event:IOErrorEvent):void {
            trace("ioErrorHandler: " + event);// if the loader fails to load the XML file this will trace out the error.
        }
        private function xmlLoaded(event:Event):void {
            var loader:URLLoader=URLLoader(event.target);// must have
            keyData = new XML(loader.data);// takes the loaded XML data and puts it in to our XML object.
            buildKeyboard();/// since the XML is loaded we now can build the keyboard since it requires the XML data.
        }
    }
}
[/as]


Breakdown of the code. This is where the fun begins.

First thing we are doing in this file is setting up the package. The name of the package is associated with the location of this file in relation to CustomKeyboard.fla. Since this file is located in the "actionscript" directory we must name the package "actionscript.
The next few lines are our import files. Since we will need to use other classes inside this class we must import them.
The final line in this code block creates our class, CustomKeyboard.
[as]
package actionscript{

import flash.display.MovieClip;
import flash.net.URLLoader;
import flash.net.URLRequest;
import flash.events.*;
import flash.text.TextField;

public class CustomKeyboard extends MovieClip {
[/as]


This next section is used to declare out class variables. Notice how they have static in front of them. The use of static specifies that a variable, constant, or method belongs to the class, rather than to instances of the class. If we don't use the static method it can cause errors later on.
[as]
static var keyData:XML = new XML; // used to store the loaded XML data.
static var keyHolder:MovieClip = new MovieClip; // used to hold all the buttons
static var textScreen:TextField = new TextField; // Our textfield that we write to.
static var shift:Boolean = false; // Tells if the shift key is down.
static var caps:Boolean = false; // Tells if the Caps Loack key is down.
[/as]


Now on to the classes constructor. This is executed when the class is created for the first time. Since we need stuff initialized when the application is executed, we put it in this function. Explanation of the methods:
buildTextScreen() creates the textfield and adds it to the stage of the application.
loadKeyboardinfo() loads the keys.xml file and once it is loaded it builds the keyboard on the screen.
setupKeyHolder() positions and adds the keyHolder movieclip to the stage.
[as]
public function CustomKeyboard() {
buildTextScreen();
loadKeyboardinfo();
setupKeyHolder();
}
[/as]


This code block shows you the three functions that are called by the constructor. Once these are called the application stops until the xml file loads. But since it loads really fast it is hard to tell.
[as]
private function buildTextScreen():void {
textScreen.x = 5; // position on the main stage
textScreen.y = 5; // position on the main stage
textScreen.width = 640; // width of the textScreen
textScreen.height = 190; // height of the textScreen
textScreen.htmlText = ""; //
textScreen.wordWrap = true; // makes the textScreen wrap text to a newline once it reaches the end.
textScreen.selectable = false; // restricts teh user from being able to select the text in textScreen
textScreen.border = true; // draws a border around the textScreen
addChild(textScreen); // adds the textScreen to the main stage
}
private function loadKeyboardinfo():void {
var xmlLoader:URLLoader = new URLLoader; // creates a loader to load the xml data.
var xmlRequester:URLRequest = new URLRequest("xml/keys.xml"); // this request object is used to tell the loader what and where to load.
xmlLoader.addEventListener(Event.COMPLETE,xmlLoaded); // adds an event listener that fires once the loader has completed loading the xml file.
xmlLoader.addEventListener(IOErrorEvent.IO_ERROR,ioErrorHandler); // this event listener fires if there is a problem finding or loading the xml file.
xmlLoader.load(xmlRequester); // starts the loading of the xml file.
}
private function setupKeyHolder():void {
keyHolder.x = 20; // positions the keyHolder movieclip on the main stage.
keyHolder.y = 200; // positions the keyHolder movieclip on the main stage.
addChild(keyHolder); // adds the keyHolder movieclip to the main stage.
}

[/as]


Next we will be looking at the event listeners that were previously setup. Now most of you coming from AS2 are new to event listeners. Well to put it simpley. If you want something to perform an action during runtime, you will most likely need to use an event listener. Lets say you want to create a button. Well your button would need many event listeners. For example: you would need a seperate one for clicking, mouse over, and mouse out. why would you need so many event listeners, well when you do any of those actions usually seperate things need to happen. If you click you may want it to go to a new website, if you mouse over you may want it to change it's appearance, and if you mouse out you may want it to go back to its default appearance. We will be getting in to MouseEvents when we look at the KeyButton class. For now we will be looking at the events that are called when the loader completes or fails.
[as]
private function ioErrorHandler(event:IOErrorEvent):void {
trace("ioErrorHandler: " + event); // if the loader fails to load the XML file this will trace out the error.
}
private function xmlLoaded(event:Event):void {
var loader:URLLoader=URLLoader(event.target); // must have
keyData = new XML(loader.data); // takes the loaded XML data and puts it in to our XML object.
buildKeyboard(); /// since the XML is loaded we now can build the keyboard since it requires the XML data.
}
[/as]


Well now we just wait until the XML loads and then the buildKeyboard() function is called. Once this is called, the keyboard is built on the screen ready to be played with. This function takes the XML data that we loaded and goes through it to build all the keys. It uses loops to go through all the nodes in the XML and takes the nodes values and assigns it to the keyButton object.
[as]
private function buildKeyboard():void {
var numKeys:Number = getNumberOfKeys(); // uses a function to find out how many keys are in the XML file.
var xPlacement:Number = 0; // used to position the keys in the keyHolder movieclip.
for (var i = 0; i < keyData.row.length(); i++) { // loop used to draw each row.
for (var j = 0; j < keyData.row[i].key.length(); j++) { // loop used to draw all the keys in a row.
var newKey:KeyButton = new KeyButton; // creates a new instance of the KeyButton class ( our onscreen button ).
newKey.code = keyData.row[i].key[j].code; // sets the newKey's code variable to the appropriate XML node value for that key.
newKey.char = keyData.row[i].key[j].char; // sets the newKey's char variable to the appropriate XML node value for that key.
newKey.shiftChar = keyData.row[i].key[j].shiftChar; // sets the newKey's shiftChar variable to the appropriate XML node value for that key.
newKey.keyWidth = checkWidth(keyData.row[i].key[j].char); // uses a function to determine the width of the key.
newKey.x = xPlacement + 5; // used to position the key on the x (up and down).
newKey.y = (newKey.keyHeight + 5) * i; // used to position the key on the y ( left and right ).
newKey.build(); // calls a function that is inside the newKey object. More info in KeyButton.as
xPlacement += newKey.keyWidth+ 5; // sets the xPlacement variable for the next pass through the loop.
keyHolder.addChild(newKey); // adds the newKey to the keyHolder movieclip. The keyHolder is already on the stage by now so the key will display.
}
xPlacement = 0; // resets the x postions once a row has been drawn so the next row starts where it is suppose to.
}
}
[/as]


Now for the final functions in this class. Quick breakdown:
  1. getNumberOfKeys(): This function goes through the XML object and counts how many keys there should be.
  2. checkWidth(char:String): This function is used to calculate the width of the key. It compares the value char (the character that will show up on the key) againts an array and a string to determine its width. The width it returned from this function and is set to the newKey's value "keyWidth".
  3. capsToggle(): This is used to toggle the caps lock key.
  4. shiftToggle(): This is used to toggle the shifht key.
[as]
private function getNumberOfKeys():Number {
var keyCounter:Number = 0;
for (var i = 0; i < keyData.row.length(); i++) {
keyCounter += keyData.row[i].key.length();
}
return keyCounter;
}
private function checkWidth(char:String):Number {
var mediumKeys:Array = new Array('Tab','Enter','Backspace','Caps Lock');
var shiftKey:String = "Shift";
var spaceKey:String = "Space Bar";
for (var m = 0; m < mediumKeys.length; m++) {
if (char == mediumKeys[m]) {
return 75;
}
}
if (char == shiftKey) {
return 95;
}
if (char == spaceKey) {
return keyHolder.width;
}
return 35;
}
        static public function capsToggle():void {
            if (caps) {
                caps = false;
            } else {
                caps = true;
            }
        }
        static public function shiftToggle():void {
            if (shift) {
                shift = false;
            } else {
                shift = true;
            }
            if (caps) {
                caps = false;
            }
        }
[/as]

Keybutton Class
[as]
package actionscript{

    import flash.display.MovieClip;
    import flash.display.Graphics;
    import flash.text.TextField;
    import flash.text.TextFieldAutoSize;
    import flash.text.TextFormat;
    import flash.events.*;

    public class KeyButton extends MovieClip {

        public var keyWidth:uint = new uint;
        public var keyHeight:uint = new uint;
        public var code:uint = new uint;
        public var char:String = new String;
        public var shiftChar:String= new String;
        public var keyColor:uint= new uint;
        public var charColor:uint= new uint;
        public var lineColor:uint= new uint;
        public var charTextField:TextField = new TextField;
        public var shiftCharTextField:TextField = new TextField;
        private var charFormat:TextFormat = new TextFormat();
        private var backGround:MovieClip = new MovieClip;

        public function KeyButton() {
            // Setup the default for the variables.
            keyWidth = 25;
            keyHeight = 35;
            code = 0;
            char = '';
            shiftChar = '';
            keyColor = 0xCCCCCC;
            charColor = 0x000000;
            lineColor = 0x888888;
            addEventListener(MouseEvent.CLICK, clicked);
            addEventListener(MouseEvent.MOUSE_OVER, hover);
            addEventListener(MouseEvent.MOUSE_OUT, leave);
            addEventListener(Event.ENTER_FRAME, checkStatus);
                        addChild(backGround);

        }
        public function build():void {
            defaultState();
            addTextFields();
            CustomKeyboard.textScreen.text = "";
        }
        private function reDraw(mouseStatus:String = ''):void {
            if (char == "Shift") {
                if (CustomKeyboard.shift) {
                    hoverState();
                } else {
                    defaultState();
                }
            } else if (char == "Caps Lock") {
                if (CustomKeyboard.caps) {
                    hoverState();
                } else {
                    defaultState();
                }
            } else {
                if (mouseStatus == "over") {
                    hoverState();
                } else if (mouseStatus == "out") {
                    defaultState();
                }
            }
        }
        private function hoverState() {
            backGround.graphics.clear();
            backGround.graphics.beginFill(keyColor-0x222222,1);
            backGround.graphics.lineStyle(0, lineColor-0x222222, 1, true);
            backGround.graphics.drawRect(0, 0, keyWidth, keyHeight);
            backGround.graphics.endFill();
        }
        private function defaultState() {
            backGround.graphics.clear();
            backGround.graphics.beginFill(keyColor,1);
            backGround.graphics.lineStyle(0, lineColor, 1, true);
            backGround.graphics.drawRect(0, 0, keyWidth, keyHeight);
            backGround.graphics.endFill();
        }
        private function addTextFields():void {
            charFormat.font = "Verdana";
            charFormat.color = charColor;
            charFormat.size = 12;

            charTextField.width = keyWidth;
            charTextField.y = 15;
            charTextField.htmlText = char;
            charTextField.setTextFormat(charFormat);
            charTextField.autoSize = TextFieldAutoSize.CENTER;
            charTextField.selectable = false;
            charTextField.mouseEnabled = false;

            charFormat.size = 10;

            shiftCharTextField.width = keyWidth/2;
            shiftCharTextField.text = shiftChar;
            shiftCharTextField.setTextFormat(charFormat);
            shiftCharTextField.autoSize = TextFieldAutoSize.CENTER;
            shiftCharTextField.selectable = false;
            shiftCharTextField.mouseEnabled = false;

            addChild(charTextField);
            addChild(shiftCharTextField);
        }
        private function addText(string:String, shiftString:String) {
            if (CustomKeyboard.shift) {
                CustomKeyboard.textScreen.appendText(shiftString);
                CustomKeyboard.shift = false;
            } else if (CustomKeyboard.caps) {
                CustomKeyboard.textScreen.appendText(shiftString);
            } else {
                CustomKeyboard.textScreen.appendText(string);
            }
        }
        private function clicked(event:MouseEvent):void {
            switch (char) {
                case "Space Bar" :
                    CustomKeyboard.textScreen.appendText(" ");
                    break;
                case "Tab" :
                    CustomKeyboard.textScreen.appendText("\t");
                    break;
                case "Backspace" :
                    CustomKeyboard.textScreen.replaceText(CustomKeyboard.textScreen.text.length-1, CustomKeyboard.textScreen.text.length,'');
                    break;
                case "Enter" :
                    CustomKeyboard.textScreen.appendText("\n");
                    break;
                case "Shift" :
                    CustomKeyboard.shiftToggle();
                    break;
                case "Caps Lock" :
                    CustomKeyboard.capsToggle();
                    CustomKeyboard.shift = false;
                    break;
                default :
                    addText(char, shiftChar);
            }
        }
        private function checkStatus(event:Event) {
            reDraw();
        }
        private function hover(event:MouseEvent) {
            reDraw("over");
        }
        private function leave(event:MouseEvent) {
            reDraw("out");
        }
    }
}
[/as]

Once again we start with creating the package and importing the needed files and starting our class file. In the construtor KeyButton() we setup our variables. These varialbes are the default variables for our class. When an instance of the class is created these variables will be used. If we want to overwrite them we just access them through the dot "." operator. Example: KeyButton.char = "Example". Now the char data for this button will read "Example". This will be displayed on top of the button for it's label.
Event Listeners
There are a few event listeners we are using here. MOUSE_OVER, MOUSE_OUT and and ENTER_FRAME. These are all used to redraw the buttons. The only one that isn't envolved is MOUSE_CLICK. This is used to write the KeyButton's char or subChar value to the textField.
[as]
        addEventListener(MouseEvent.CLICK, clicked);
        addEventListener(MouseEvent.MOUSE_OVER, hover);
        addEventListener(MouseEvent.MOUSE_OUT, leave);
        addEventListener(Event.ENTER_FRAME, checkStatus);

        private function checkStatus(event:Event) {
            reDraw();
        }
        private function hover(event:MouseEvent) {
            reDraw("over");
        }
        private function leave(event:MouseEvent) {
            reDraw("out");
        }
        private function clicked(event:MouseEvent):void {
            switch (char) {
                case "Space Bar" :
                    CustomKeyboard.textScreen.appendText(" ");
                    break;
                case "Tab" :
                    CustomKeyboard.textScreen.appendText("\t");
                    break;
                case "Backspace" :
                    CustomKeyboard.textScreen.replaceText(CustomKeyboard.textScreen.text.length-1, CustomKeyboard.textScreen.text.length,'');
                    break;
                case "Enter" :
                    CustomKeyboard.textScreen.appendText("\n");
                    break;
                case "Shift" :
                    CustomKeyboard.shiftToggle();
                    break;
                case "Caps Lock" :
                    CustomKeyboard.capsToggle();
                    CustomKeyboard.shift = false;
                    break;
                default :
                    addText(char, shiftChar);
            }
        }
[/as]


The reDraw() function is used to change the look of the buttons. It uses the boolean variables from the main class to see how to draw the special keys (Shifht and Caps Lock) and also controls how the normal buttons look. This function is called when a user hovers over and leaves a button.
[as]
        private function reDraw(mouseStatus:String = ''):void {
            if (char == "Shift") {
                if (CustomKeyboard.shift) {
                    hoverState();
                } else {
                    defaultState();
                }
            } else if (char == "Caps Lock") {
                if (CustomKeyboard.caps) {
                    hoverState();
                } else {
                    defaultState();
                }
            } else {
                if (mouseStatus == "over") {
                    hoverState();
                } else if (mouseStatus == "out") {
                    defaultState();
                }
            }
        }
[/as]

addText(string:String, shiftString:String) is used to add the right character to the textScreen object. It adds the char string if the caps lock or the shift key are not activated, otherwise it adds the shiftChar.
[as]
        private function addText(string:String, shiftString:String) {
            if (CustomKeyboard.shift) {
                CustomKeyboard.textScreen.appendText(shiftString);
                CustomKeyboard.shift = false;
            } else if (CustomKeyboard.caps) {
                CustomKeyboard.textScreen.appendText(shiftString);
            } else {
                CustomKeyboard.textScreen.appendText(string);
            }
        }
[/as]


Well what is a button whitout a label? Well this is where addTextFields() comes in to play. It adds two textFields to the button. One for the char and one for the subChar.
[as]
        private function addTextFields():void {
            charFormat.font = "Verdana";
            charFormat.color = charColor;
            charFormat.size = 12;

            charTextField.width = keyWidth;
            charTextField.y = 15;
            charTextField.htmlText = char;
            charTextField.setTextFormat(charFormat);
            charTextField.autoSize = TextFieldAutoSize.CENTER;
            charTextField.selectable = false;
            charTextField.mouseEnabled = false;

            charFormat.size = 10;

            shiftCharTextField.width = keyWidth/2;
            shiftCharTextField.text = shiftChar;
            shiftCharTextField.setTextFormat(charFormat);
            shiftCharTextField.autoSize = TextFieldAutoSize.CENTER;
            shiftCharTextField.selectable = false;
            shiftCharTextField.mouseEnabled = false;

            addChild(charTextField);
            addChild(shiftCharTextField);
        }
[/as]


Now on to the functions that make the look of the buttons. This is where hoverState() and defaultState() come in. These functions use the graphics class to draw rectangles in the backGround movieclip. The graphics.clear() function clears all of the graphics objects from the backGround movieclip. then we use the drawRect function to draw a box. We use the class variables to set the width, height and colors of the rectangle.
[as]
        private function hoverState() {
            backGround.graphics.clear();
            backGround.graphics.beginFill(keyColor-0x222222,1);
            backGround.graphics.lineStyle(0, lineColor-0x222222, 1, true);
            backGround.graphics.drawRect(0, 0, keyWidth, keyHeight);
            backGround.graphics.endFill();
        }
        private function defaultState() {
            backGround.graphics.clear();
            backGround.graphics.beginFill(keyColor,1);
            backGround.graphics.lineStyle(0, lineColor, 1, true);
            backGround.graphics.drawRect(0, 0, keyWidth, keyHeight);
            backGround.graphics.endFill();
        }
[/as]

Conclusion
This concludes this tutorial. I hope that you have learned something and will be able to use that knowledge to advance your learning of Actionscript 3.0. In future tutorials I will be going in to more depth into Event Listeners. Including adding keyboard events to allow you to type and having this keyboard application respond to those events. I will also be creating a series of game and music tutorials. So keep checking Actionscript.org for future tutorials. And if you have suggestions for future tutorials or want to see a specific topic discussed, drop me a PM on the forums.