Thursday, October 11, 2012

Creating Advanced Alert Window class: Part 39

In this tutorial we will add the ability to add default button index for alert windows (for tab navigation), as well as improve the tab navigation itself.

By adding a default button index I mean setting an index of the button that the developer wants to be selected by default. So, if the alert pops up and the user simply presses Enter on his keyboard, the default button is pressed. The default index is 0 by default (which represents the first button in the array).

There are a few things we need to keep in mind while implementing the feature.

The first important thing is that when there are more than 1 alert windows present, when the user closes the first one, we need to update the new button index (focusedButton variable) according to the next window's default button index.

The second thing is related to how we handle tab navigation. Right now the kDown function increases focusedButton by 1 and executes all the code in it using that value, which is why we have focusedButton set to -1 by default (-1 + 1 = 0, and 0 is the index we want to start with). We're going to modify the code, so that the focusedButton value is increased by 1 ONLY if there is a button in the window, which has "over" property set to true.

This means, that if the default button index is 1 (second button), and there are no buttons currently rolled over or selected using tab, if the user presses Enter - the second button is pressed. If they press Tab, the second button is selected, instead of third, so that the user can see what button is highlighted and press enter again to press it.

So, as long as nothing is initially selected: pressing Enter = pressing Tab and then Enter.

This makes the navigation more user-friendly and intuitive.

Let's get started. Go to AdvAlertManager class, find the alert() function and add a new parameter to it right after the buttons one - defaultButton:int = 0.

public function alert(text:String, title:String = "", buttons:Array = null, defaultButton:int = 0, closeHandler:Function = null, width:int = 0, height:int = 0, position:Point = null, minHeight:int = 0, maxHeight:int = 0, maxWidth:int = 0, horizontalStretch:Boolean = true, openSound:Sound = null):AdvAlertWindow {

Add a new parameter to the object that is added to the windows array in alert() function - set the attribute name to defButton and the value to defaultButton:

windows.push( {window:w, blur:b, onclose:closeHandler, defButton:defaultButton} );

In the same function set focusedButton to defaultButton:

focusedButton = defaultButton;

Full function:

/**
 * Create an alert window.
 * @paramtext Text value of the alert window.
 * @paramtitle Title value of the alert window.
 * @parambuttons (Optional) Array of AdvAlertButton objects that represent buttons in the alert window.
 * @paramdefaultButton (Optional) Index of the default button (used for tab navigation).
 * @paramcloseHandler (Optional) Close handling function. Must receive a string value as parameter (which will hold the label of the button that was clicked).
 * @paramwidth (Optional) Width of the alert window. If 0, set to default.
 * @paramheight (Optional) Height of the alert window. If 0, set to default. If default is 0, window will be auto sized.
 * @paramposition (Optional) Coordinates of top-left corner of the alert window. If not specified - the window is centered.
 * @paramminHeight (Optional) The minimum height value of the alert window. If 0, set to default.
 * @parammaxHeight (Optional) The maximum height value of the alert window. If 0, set to default.
 * @parammaxWidth (Optional) The maximum width value of the alert window. If 0, set to default.
 * @paramhorizontalStretch (Optional) If set to true, will stretch the window horizontally if the text doesn't fit and the height has already reached maxHeight.
 * @paramopenSound (Optional) Sound to play when the window opens. Specified default sound plays by default.
 */

public function alert(text:String, title:String = "", buttons:Array = null, defaultButton:int = 0, closeHandler:Function = null, width:int = 0, height:int = 0, position:Point = null, minHeight:int = 0, maxHeight:int = 0, maxWidth:int = 0, horizontalStretch:Boolean = true, openSound:Sound = null):AdvAlertWindow {
if (width == 0) width = defWidth;
if (height == 0) height = defHeight;
if (minHeight == 0) minHeight = defMinHeight;
if (maxHeight == 0) maxHeight = defMaxHeight;
if (maxWidth == 0) maxWidth = defMaxWidth;
if (openSound != null) {
openSound.play();
}else
if (openingSound != null) {
openingSound.play();
}
if (position == null) position = new Point((pWidth / 2) - (width / 2), (pHeight / 2) - (height / 2));
if (buttons == null) buttons = [new AdvAlertButton("OK")];
for (var i:int = buttons.length - 1; i >= 0; i--) {
if (!buttons[i] is AdvAlertButton) {
throw new Error("An item in 'buttons' array is not an AdvAlertButton instance. Ignoring...");
buttons.splice(i, 1);
}else {
buttons[i].addEventListener(MouseEvent.CLICK, buttonHandler);
}
}
var w:AdvAlertWindow = new AdvAlertWindow(text, title, width, height, position, skin, buttons, bSkin, pWidth, pHeight, minHeight, maxHeight, maxWidth, horizontalStretch);
var b:AdvAlertBlur = new AdvAlertBlur(pWidth, pHeight, skin);
var currentIndex:int = windows.length;
windows.push( {window:w, blur:b, onclose:closeHandler, defButton:defaultButton} );
defaultContainer.addChild(b);
defaultContainer.addChild(w);

topWindow = w;
focusedButton = defaultButton;
if (tabDisable) {
defaultContainer.tabChildren = false;
defaultContainer.tabEnabled = false;
}

function buttonHandler(evt:MouseEvent):void {
closeWindow(currentIndex, closeHandler, evt.currentTarget as AdvAlertButton);
}
return w;
}

Go to kDown() function. Inside the keyCode==9 if...statement add 2 variables - needIncrease and i.

Set needIncrease to false by default. Then loop through all buttons, and if at least one button has its 'over' property set to true - set needIncrease to true.

Check if needIncrease is true, if so - increase focusedButton by 1.

private function kDown(evt:KeyboardEvent):void {
if(tabDisable){
if (evt.keyCode == 9 && topWindow != null && topWindow.buttons.length > 0) {
var needIncrease:Boolean = false;
var i:int;
for (i = 0; i < topWindow.buttons.length; i++) {
if (topWindow.buttons[i].over == true) needIncrease = true;
}
if (needIncrease) focusedButton++;
if (focusedButton >= topWindow.buttons.length) {
focusedButton = 0;
}
for (i = 0; i < topWindow.buttons.length; i++) {
topWindow.buttons[i].over = false;
topWindow.buttons[i].updateDraw();
}
topWindow.buttons[focusedButton].over = true;
topWindow.buttons[focusedButton].updateDraw();
}
if (evt.keyCode == 13 && topWindow != null && topWindow.buttons.length > 0) {
var currentIndex:int = windows.length - 1;
var closeHandler:Function = windows[currentIndex].onclose;
closeWindow(currentIndex, closeHandler, topWindow.buttons[focusedButton]);
}
}
}

Now go to closeWindow() function. Remove the line that sets focusedButton to -1, instead add a new line to windows.length>0 if...statement and set focusedButton to the defButton value of the next window:

private function closeWindow(currentIndex:int, closeHandler:Function, currentButton:AdvAlertButton):void {
if (closeHandler != null && currentButton != null) closeHandler.call(currentButton, currentButton.txt);
windows[currentIndex].window.parent.removeChild(windows[currentIndex].window);
windows[currentIndex].blur.parent.removeChild(windows[currentIndex].blur);
windows.splice(currentIndex, 1);
if (windows.length > 0) {
focusedButton = windows[windows.length - 1].defButton;
topWindow = windows[windows.length - 1].window;
}
if (windows.length == 0) topWindow = null;
if (tabDisable && windows.length==0) {
defaultContainer.tabChildren = true;
defaultContainer.tabEnabled = true;
}
}

And that's it!

Full AdvAlertManager class code:

package com.kircode.AdvAlert 
{
import flash.display.DisplayObjectContainer;
import flash.display.MovieClip;
import flash.display.Stage;
import flash.events.MouseEvent;
import flash.geom.Point;
import flash.events.KeyboardEvent;
import flash.events.Event;
import flash.media.Sound;
/**
 * Advanced Alert window manager.
 * @author Kirill Poletaev
 */
public class AdvAlertManager 
{
private var windows:Array;
private var defaultContainer:DisplayObjectContainer;
private var pWidth:int;
private var pHeight:int;
private var skin:AdvAlertSkin;
private var bSkin:AdvAlertButtonSkin;
private var tabDisable:Boolean;
private var topWindow:AdvAlertWindow;
private var focusedButton:int;
private var stg:Stage;
private var openingSound:Sound;
private var defWidth:int;
private var defHeight:int;
private var defMinHeight:int;
private var defMaxHeight:int;
private var defMaxWidth:int;

/**
 * AdvAlertManager constructor.
 * @paramdefaultWindowContainer Parent container of alert windows.
 * @paramstage Stage reference.
 * @paramwindowSkin Default skin for alert windows.
 * @parambuttonSkin Default skin for buttons in this window.
 * @paramopenSound Default window open sound.
 * @paramdisableTab Disable tab focusing on the parent container when an alert window is visible. This also enables tab navigation for the alert window's buttons.
 * @paramdefaultWidth Set default width for all window alerts. 300 by default.
 * @paramdefaultHeight Set default height for all window alerts. 0 by default. If 0, the window is auto-sized based on the text length.
 * @paramdefaultMinHeight Set default minimal height for all window alerts. 0 by default.
 * @paramdefaultMaxHeight Set default maximal height for all window alerts. 0 by default. If 0, automatically set to the height of the stage.
 * @paramdefaultMaxWidth Set default maximal width for all window alerts. 0 by default. If 0, automatically set to the width of the stage.
 */

public function AdvAlertManager(defaultWindowContainer:DisplayObjectContainer, stage:Stage, windowSkin:AdvAlertSkin = null, buttonSkin:AdvAlertButtonSkin = null, openSound:Sound = null, disableTab:Boolean = true, defaultWidth:int = 300, defaultHeight:int = 0, defaultMinHeight:int = 0, defaultMaxHeight:int = 0, defaultMaxWidth:int = 0) 
{
defWidth = defaultWidth;
defHeight = defaultHeight;
defMinHeight = defaultMinHeight;
defMaxHeight = defaultMaxHeight;
defMaxWidth = defaultMaxWidth;
if (defMaxHeight == 0) defMaxHeight = stage.stageHeight;
if (defMaxWidth == 0) defMaxWidth = stage.stageWidth;
openingSound = openSound;
skin = windowSkin;
bSkin = buttonSkin;
tabDisable = disableTab;
if (skin == null) skin = new AdvAlertSkin();
if (bSkin == null) bSkin = new AdvAlertButtonSkin();
defaultContainer = defaultWindowContainer;
pWidth = stage.stageWidth;
pHeight = stage.stageHeight;
stg = stage;
windows = [];
stg.addEventListener(KeyboardEvent.KEY_DOWN, kDown);
}

private function kDown(evt:KeyboardEvent):void {
if(tabDisable){
if (evt.keyCode == 9 && topWindow != null && topWindow.buttons.length > 0) {
var needIncrease:Boolean = false;
var i:int;
for (i = 0; i < topWindow.buttons.length; i++) {
if (topWindow.buttons[i].over == true) needIncrease = true;
}
if (needIncrease) focusedButton++;
if (focusedButton >= topWindow.buttons.length) {
focusedButton = 0;
}
for (i = 0; i < topWindow.buttons.length; i++) {
topWindow.buttons[i].over = false;
topWindow.buttons[i].updateDraw();
}
topWindow.buttons[focusedButton].over = true;
topWindow.buttons[focusedButton].updateDraw();
}
if (evt.keyCode == 13 && topWindow != null && topWindow.buttons.length > 0) {
var currentIndex:int = windows.length - 1;
var closeHandler:Function = windows[currentIndex].onclose;
closeWindow(currentIndex, closeHandler, topWindow.buttons[focusedButton]);
}
}
}

/**
 * Close the top alert window.
 */

public function closeLast():void {
var currentIndex:int = windows.length - 1;
var closeHandler:Function = windows[currentIndex].onclose;
closeWindow(currentIndex, closeHandler, null);
}

private function closeWindow(currentIndex:int, closeHandler:Function, currentButton:AdvAlertButton):void {
if (closeHandler != null && currentButton != null) closeHandler.call(currentButton, currentButton.txt);
windows[currentIndex].window.parent.removeChild(windows[currentIndex].window);
windows[currentIndex].blur.parent.removeChild(windows[currentIndex].blur);
windows.splice(currentIndex, 1);
if (windows.length > 0) {
focusedButton = windows[windows.length - 1].defButton;
topWindow = windows[windows.length - 1].window;
}
if (windows.length == 0) topWindow = null;
if (tabDisable && windows.length==0) {
defaultContainer.tabChildren = true;
defaultContainer.tabEnabled = true;
}
}

/**
 * Set skin of all future created alert windows.
 * @paramwindowSkin Reference to an AdvAlertSkin object.
 */

public function setSkin(windowSkin:AdvAlertSkin):void {
skin = windowSkin;
}

/**
 * Set skin of all buttons of future created alert windows.
 * @parambuttonSkin Reference to an AdvAlertButtonSkin object.
 */

public function setButtonSkin(buttonSkin:AdvAlertButtonSkin):void {
bSkin = buttonSkin;
}

/**
 * Set new options for the alert manager object - has the same parameters as the constructor.
 * @paramdefaultWindowContainer Parent container of alert windows.
 * @paramstage Stage reference.
 * @paramwindowSkin Default skin for alert windows.
 * @parambuttonSkin Default skin for buttons in this window.
 * @paramopenSound Default window open sound.
 * @paramdisableTab Disable tab focusing on the parent container when an alert window is visible. This also enables tab navigation for the alert window's buttons.
 * @paramdefaultWidth Set default width for all window alerts. 300 by default.
 * @paramdefaultHeight Set default height for all window alerts. 0 by default. If 0, the window is auto-sized based on the text length.
 * @paramdefaultMinHeight Set default minimal height for all window alerts. 0 by default.
 * @paramdefaultMaxHeight Set default maximal height for all window alerts. 0 by default. If 0, automatically set to the height of the parent.
 * @paramdefaultMaxWidth Set default maximal width for all window alerts. 0 by default. If 0, automatically set to the width of the stage.
 */

public function reset(defaultWindowContainer:DisplayObjectContainer, stage:Stage, windowSkin:AdvAlertSkin = null, buttonSkin:AdvAlertButtonSkin = null, openSound:Sound = null, disableTab:Boolean = true, defaultWidth:int = 300, defaultHeight:int = 0, defaultMinHeight:int = 0, defaultMaxHeight:int = 0, defaultMaxWidth:int = 0) 
{
defWidth = defaultWidth;
defHeight = defaultHeight;
defMinHeight = defaultMinHeight;
defMaxHeight = defaultMaxHeight;
defMaxWidth = defaultMaxWidth;
if (defMaxHeight == 0) defMaxHeight = stage.stageHeight;
if (defMaxWidth == 0) defMaxWidth = stage.stageWidth;
openingSound = openSound;
skin = windowSkin;
bSkin = buttonSkin;
tabDisable = disableTab;
if (skin == null) skin = new AdvAlertSkin();
if (bSkin == null) bSkin = new AdvAlertButtonSkin();
defaultContainer = defaultWindowContainer;
pWidth = stage.stageWidth;
pHeight = stage.stageHeight;
stg = stage;
}

/**
 * Create an alert window.
 * @paramtext Text value of the alert window.
 * @paramtitle Title value of the alert window.
 * @parambuttons (Optional) Array of AdvAlertButton objects that represent buttons in the alert window.
 * @paramdefaultButton (Optional) Index of the default button (used for tab navigation).
 * @paramcloseHandler (Optional) Close handling function. Must receive a string value as parameter (which will hold the label of the button that was clicked).
 * @paramwidth (Optional) Width of the alert window. If 0, set to default.
 * @paramheight (Optional) Height of the alert window. If 0, set to default. If default is 0, window will be auto sized.
 * @paramposition (Optional) Coordinates of top-left corner of the alert window. If not specified - the window is centered.
 * @paramminHeight (Optional) The minimum height value of the alert window. If 0, set to default.
 * @parammaxHeight (Optional) The maximum height value of the alert window. If 0, set to default.
 * @parammaxWidth (Optional) The maximum width value of the alert window. If 0, set to default.
 * @paramhorizontalStretch (Optional) If set to true, will stretch the window horizontally if the text doesn't fit and the height has already reached maxHeight.
 * @paramopenSound (Optional) Sound to play when the window opens. Specified default sound plays by default.
 */

public function alert(text:String, title:String = "", buttons:Array = null, defaultButton:int = 0, closeHandler:Function = null, width:int = 0, height:int = 0, position:Point = null, minHeight:int = 0, maxHeight:int = 0, maxWidth:int = 0, horizontalStretch:Boolean = true, openSound:Sound = null):AdvAlertWindow {
if (width == 0) width = defWidth;
if (height == 0) height = defHeight;
if (minHeight == 0) minHeight = defMinHeight;
if (maxHeight == 0) maxHeight = defMaxHeight;
if (maxWidth == 0) maxWidth = defMaxWidth;
if (openSound != null) {
openSound.play();
}else
if (openingSound != null) {
openingSound.play();
}
if (position == null) position = new Point((pWidth / 2) - (width / 2), (pHeight / 2) - (height / 2));
if (buttons == null) buttons = [new AdvAlertButton("OK")];
for (var i:int = buttons.length - 1; i >= 0; i--) {
if (!buttons[i] is AdvAlertButton) {
throw new Error("An item in 'buttons' array is not an AdvAlertButton instance. Ignoring...");
buttons.splice(i, 1);
}else {
buttons[i].addEventListener(MouseEvent.CLICK, buttonHandler);
}
}
var w:AdvAlertWindow = new AdvAlertWindow(text, title, width, height, position, skin, buttons, bSkin, pWidth, pHeight, minHeight, maxHeight, maxWidth, horizontalStretch);
var b:AdvAlertBlur = new AdvAlertBlur(pWidth, pHeight, skin);
var currentIndex:int = windows.length;
windows.push( {window:w, blur:b, onclose:closeHandler, defButton:defaultButton} );
defaultContainer.addChild(b);
defaultContainer.addChild(w);

topWindow = w;
focusedButton = defaultButton;
if (tabDisable) {
defaultContainer.tabChildren = false;
defaultContainer.tabEnabled = false;
}

function buttonHandler(evt:MouseEvent):void {
closeWindow(currentIndex, closeHandler, evt.currentTarget as AdvAlertButton);
}
return w;
}

}

}

Now you acn go to main.as and try this code, which creates 2 windows with 3 buttons each, but different default indices. If you just press enter (or tab and then enter), you'll see that different buttons are selected by default in each window.

private function init(evt:Event):void {
var mySkin:AdvAlertSkin = new AdvAlertSkin();
mySkin.addContent(warning_icon, new Point(5, 40));
mySkin.textPadding.left = 140;
mySkin.titleFilters = [new DropShadowFilter(0, 0, 0, 1, 3, 3, 2, 3)];
mySkin.textFilters = [new DropShadowFilter(1, 45, 0, 0.4, 0, 0, 5, 1)];

var myButtonSkin:AdvAlertButtonSkin = new AdvAlertButtonSkin();
myButtonSkin.textFilters = [new DropShadowFilter(1, 45, 0, 0.4, 0, 0, 5, 1)];
myButtonSkin.hover_textFilters = [new DropShadowFilter(1, 45, 0, 0.4, 0, 0, 5, 1), new DropShadowFilter(0, 0, 0, 1, 3, 3, 1, 3)];

AlertManager = new AdvAlertManager(this, stage, mySkin, myButtonSkin);
AlertManager.alert("A window with 3 buttons. Default index: 1", "Example alert!", [new AdvAlertButton("One"), new AdvAlertButton("Two"), new AdvAlertButton("Three")], 1, onClose, 300, 200);
AlertManager.alert("A window with 3 buttons. Default index: 2", "Example alert!", [new AdvAlertButton("One"), new AdvAlertButton("Two"), new AdvAlertButton("Three")], 2, onClose, 300, 200);
}

private function onClose(str:String):void {
trace(str);
}

Thanks for reading!

No comments:

Post a Comment