/* Copyright (c) 2009 Yahoo! Inc. All rights reserved. The copyrights embodied in the content of this file are licensed under the BSD (revised) open source license */ package com.yahoo.astra.fl.controls { import com.yahoo.astra.fl.controls.tabBarClasses.TabButton; import com.yahoo.astra.fl.events.TabBarEvent; import fl.controls.Button; import fl.core.InvalidationType; import fl.core.UIComponent; import fl.data.DataProvider; import fl.data.SimpleCollectionItem; import fl.events.DataChangeEvent; import fl.managers.IFocusManagerComponent; import flash.events.Event; import flash.events.FocusEvent; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.text.TextFormat; import flash.text.TextFormatAlign; import flash.ui.Keyboard; //-------------------------------------- // Events //-------------------------------------- /** * Dispatched when the user rolls the pointer off of an tab in the component. * * @eventType com.yahoo.astra.fl.events.TabBarEvent.ITEM_ROLL_OUT * * @see #event:itemRollOver */ [Event(name="itemRollOut", type="com.yahoo.astra.fl.events.TabBarEvent")] /** * Dispatched when the user rolls the pointer over an tab in the component. * * @eventType com.yahoo.astra.fl.events.TabBarEvent.ITEM_ROLL_OVER * * @see #event:itemRollOut */ [Event(name="itemRollOver", type="com.yahoo.astra.fl.events.TabBarEvent")] /** * Dispatched when the user rolls the pointer over the component. * * @eventType flash.events.MouseEvent.ROLL_OVER * * @see #event:rollOut */ [Event(name="rollOver", type="flash.events.MouseEvent")] /** * Dispatched when the user rolls the pointer off of the component. * * @eventType flash.events.MouseEvent.ROLL_OUT * * @see #event:rollOver */ [Event(name="rollOut", type="flash.events.MouseEvent")] /** * Dispatched when the user clicks an item in the component. * *

The click event is dispatched before the value * of the component is changed. To identify the tabs that was clicked, * use the properties of the event object; do not use the selectedIndex * and selectedItem properties.

* * @eventType com.yahoo.astra.fl.events.TabBarEvent.ITEM_CLICK */ [Event(name="itemClick", type="com.yahoo.astra.fl.events.TabBarEvent")] /** * Dispatched when a different item is selected in the TabBar. * * @eventType flash.events.Event.CHANGE */ [Event(name="change", type="flash.events.Event")] //-------------------------------------- // Styles //-------------------------------------- /** * The TextFormat object to use to render the component label when a tab is selected. * * @default TextFormat("_sans", 11, 0x000000, false, false, false, '', '', TextFormatAlign.LEFT, 0, 0, 0, 0) * * @see flash.text.TextFormat TextFormat */ [Style(name="selectedTextFormat", type="flash.text.TextFormat")] /** * A collection of buttons, or tabs, that may be used for navigation. * * @see fl.data.DataProvider */ public class TabBar extends UIComponent implements IFocusManagerComponent { //-------------------------------------- // Static Properties //-------------------------------------- /** * @private */ private static var defaultStyles:Object = { selectedTextFormat: null }; /** * @private */ private static const TAB_STYLES:Object = { embedFonts: "embedFonts", disabledTextFormat: "disabledTextFormat", textFormat: "textFormat", selectedTextFormat: "selectedTextFormat", textPadding: "textPadding" }; //-------------------------------------- // Static Methods //-------------------------------------- /** * @private * Creates the Accessibility class. * This method is called from UIComponent. */ public static var createAccessibilityImplementation:Function; /** * @private * @copy fl.core.UIComponent#getStyleDefinition() */ public static function getStyleDefinition():Object { return mergeStyles(defaultStyles, UIComponent.getStyleDefinition()); } //-------------------------------------- // Constructor //-------------------------------------- /** * Constructor. */ public function TabBar() { super(); this.focusEnabled = true; this.tabEnabled = true; this.tabChildren = false; this.addEventListener(KeyboardEvent.KEY_DOWN, navigationKeyDownHandler); } //-------------------------------------- // Properties //-------------------------------------- /** * @private * Displays a message in live preview mode when there is no dataProvider. */ private var _livePreviewMessage:TabButton; /** * @private * When redrawing, buttons are saved in this cache for reuse. */ protected var _cachedButtons:Array = []; /** * @private * Storage for the buttons displayed in the TabBar. */ protected var buttons:Array = []; /** * @private * Storage for the renderer (tab) styles. */ protected var rendererStyles:Object = {}; /** * @private */ protected var lastFocusIndex:int = -1; /** * @private * Storage for the focusIndex property. */ protected var _focusIndex:int = -1; /** * @private * The index of the currently focused tab (used for keyboard navigation). */ public function get focusIndex():int { return this._focusIndex; } /** * @private */ public function set focusIndex(value:int):void { this._focusIndex = value; this.invalidate("focus"); //internal event used for accessibility //similar implementation in Flex TabBar control. this.dispatchEvent(new Event("focusUpdate")); } /** * @private * Like fl.controls.List, the purpose of this variable is * only to make sure the SimpleCollectionItem is included. */ private var collectionItemImport:SimpleCollectionItem; /** * @private * Storage for the dataProvider property. */ protected var _dataProvider:DataProvider; /** * Gets or sets the data model of the list of items to be viewed. A data provider * can be shared by multiple list-based components. Changes to the data provider * are immediately available to all components that use it as a data source. * * @default null */ public function get dataProvider():DataProvider { return this._dataProvider; } [Collection(collectionClass="fl.data.DataProvider", collectionItem="fl.data.SimpleCollectionItem", identifier="item")] /** * @private */ public function set dataProvider(value:DataProvider):void { if(this._dataProvider) { this._dataProvider.removeEventListener(DataChangeEvent.DATA_CHANGE, dataChangeHandler); } this._dataProvider = value; if(this._dataProvider) { this._dataProvider.addEventListener(DataChangeEvent.DATA_CHANGE, dataChangeHandler, false, 0, true); } this.invalidate(InvalidationType.DATA); } /** * @private * Storage for the labelField property. */ protected var _labelField:String = "label"; [Inspectable(defaultValue="label")] /** * Gets or sets the name of the field in the dataProvider object * to be displayed as the label in the tabs. * *

By default, the component displays the label property * of each dataProvider item. If the dataProvider * items do not contain a label property, you can set the * labelField property to use a different property.

* *

Note: The labelField property is not used * if the labelFunction property is set to a callback function.

* * @default "label" * * @see #labelFunction */ public function get labelField():String { return this._labelField; } /** * @private */ public function set labelField(value:String):void { if(this._labelField != value) { this._labelField = value; this.invalidate(InvalidationType.DATA); } } /** * @private * Storage for the labelFunction property. */ protected var _labelFunction:Function = null; /** * Gets or sets the function to be used to obtain the label for the item. * *

By default, the component displays the label property * for a dataProvider item. But some data sets may not have * a label field or may not have a field whose value * can be used as a label without modification. For example, a given data * set might store full names but maintain them in lastName and * firstName fields. In such a case, this property could be * used to set a callback function that concatenates the values of the * lastName and firstName fields into a full * name string to be displayed.

* *

Note: The labelField property is not used * if the labelFunction property is set to a callback function.

* * @default null */ public function get labelFunction():Function { return this._labelFunction; } /** * @private */ public function set labelFunction(value:Function):void { if(this._labelFunction != value) { this._labelFunction = value; this.invalidate(InvalidationType.DATA); } } /** * @private * Storage for the selectedIndex property. */ protected var _selectedIndex:int = 0; [Inspectable] /** * Gets or sets the index of the tab that is selected. Only one item can be selected * at a time. * *

A value of -1 indicates that no tab is selected.

* *

When ActionScript is used to set this property, the item at the specified index * replaces the current selection. When the selection is changed programmatically, * a change event object is not dispatched.

* * @see #selectedItem * * @default 0 */ public function get selectedIndex():int { return this._selectedIndex; } /** * @private */ public function set selectedIndex(value:int):void { if(value < 0 || value >= this._dataProvider.length) { value = -1; } if(this._selectedIndex != value) { this._selectedIndex = value; this.focusIndex = value; this.invalidate(); this.dispatchEvent(new Event(Event.CHANGE)); } } /** * Gets or sets the item that was selected. * *

If no selection is made, the value of this property is null.

* * @see #selectedIndex */ public function get selectedItem():Object { if(this.selectedIndex >= 0) { return this._dataProvider.getItemAt(this.selectedIndex); } return null; } /** * @private */ public function set selectedItem(value:Object):void { var index:int = this._dataProvider.getItemIndex(value); this.selectedIndex = index; } /** * @private * Storage for the autoSizeTabsToTextWidth property. */ private var _autoSizeTabsToTextWidth:Boolean = true; [Inspectable(defaultValue=false)] /** * If true, the width value of the TabBar will be ignored. The tabs * will determine their size based on the size of the text they display. * If false, the tabs will stay in the bounds of the TabBar. The text * may be truncated. */ public function get autoSizeTabsToTextWidth():Boolean { return this._autoSizeTabsToTextWidth; } /** * @private */ public function set autoSizeTabsToTextWidth(value:Boolean):void { if(this._autoSizeTabsToTextWidth != value) { this._autoSizeTabsToTextWidth = value; this.invalidate(); } } /** * @private * Storage for the selectionFollowsFocus property. */ private var _selectionFollowsFocus:Boolean = true; [Inspectable(defaultValue=true)] /** * If true, selection will follow keyboard focus. If false, a tab * must be manually selected after changing keyboard focus. */ public function get selectionFollowsKeyboardFocus():Boolean { return this._selectionFollowsFocus; } /** * @private */ public function set selectionFollowsKeyboardFocus(value:Boolean):void { if(this._selectionFollowsFocus != value) { this._selectionFollowsFocus = value; this.invalidate(); } } //-------------------------------------- // Public Methods //-------------------------------------- /** * Returns the index of the given data object. * * @param item The object to be rendered. * @return the index of the object in the data provider. -1 if it cannot be found. */ public function itemToIndex(item:Object):int { return this._dataProvider.getItemIndex(item); } /** * Retrieves the string that the renderer displays for the given data object * based on the labelField and labelFunction properties. * *

Note: The labelField is not used * if the labelFunction property is set to a callback function.

* * @param item The object to be rendered. * * @return The string to be displayed based on the data. */ public function itemToLabel(item:Object):String { if(this.labelFunction != null) { this.labelFunction(item, this.itemToIndex(item)); } else if(this.labelField && item.hasOwnProperty(this.labelField)) { return item[this.labelField]; } return ""; } /** * Returns the data object corresponding to an index. * * @param index a zero-based index * @return the item at the specified index within the data provider. */ public function indexToItem(index:int):Object { return this._dataProvider.getItemAt(index); } /** * Retrieves a style that is set on the renderers in the tabbar. * * @param name The name of the style to be retrieved. */ public function getRendererStyle(name:String):Object { return this.rendererStyles[name]; } /** * Sets a style on the renderers in the list. * * @param name The name of the style to be set. * @param value The value of the style to be set. */ public function setRendererStyle(name:String, value:Object):void { if(this.rendererStyles[name] == value) { return; } this.rendererStyles[name] = value; this.invalidate(InvalidationType.RENDERER_STYLES); } /** * Clears a style that is set on the renderers in the list. * * @param name The name of the style to be cleared. */ public function clearRendererStyle(name:String):void { this.rendererStyles[name] = null; this.invalidate(InvalidationType.RENDERER_STYLES); } //-------------------------------------- // Protected Methods //-------------------------------------- /** * @private */ override protected function initializeAccessibility():void { if(TabBar.createAccessibilityImplementation != null) { TabBar.createAccessibilityImplementation(this); } } /** * @private */ override protected function configUI():void { super.configUI(); if(this.isLivePreview) { //special case for live previews with no data. this._livePreviewMessage = new TabButton(); this._livePreviewMessage.label = "No live preview data"; this.addChild(this._livePreviewMessage); } } /** * @private */ override protected function draw():void { if(this.isLivePreview) { this._livePreviewMessage.visible = !this.dataProvider || this.dataProvider.length == 0; this._livePreviewMessage.width = this.autoSizeTabsToTextWidth ? NaN : this.width; this._livePreviewMessage.height = this.height; this._livePreviewMessage.drawNow(); } var dataInvalid:Boolean = this.isInvalid(InvalidationType.DATA); if(dataInvalid) { this.createCache(); if(this._dataProvider) { this.updateButtons(); } this.clearCache(); } this.drawButtons(); super.draw(); } /** * @private * * Updates properties of the buttons. */ protected function updateButtons():void { var buttonCount:int = this._dataProvider.length; for(var i:int = 0; i < buttonCount; i++) { var button:TabButton = this.getButton(); this.buttons.push(button); var item:Object = this._dataProvider.getItemAt(i); button.label = this.itemToLabel(item); button.buttonMode = this.buttonMode; button.useHandCursor = this.useHandCursor; } } /** * @private * * Either retrieves a button from the cache or creates a new one. */ protected function getButton():TabButton { var button:TabButton; if(this._cachedButtons.length > 0) { button = this._cachedButtons.shift() as TabButton; } else { button = new TabButton(); button.toggle = true; button.focusEnabled = false; button.addEventListener(Event.CHANGE, buttonChangeHandler, false, 0, true); button.addEventListener(MouseEvent.CLICK, buttonClickHandler, false, 0, true); button.addEventListener(MouseEvent.ROLL_OVER, buttonRollOverHandler, false, 0, true); button.addEventListener(MouseEvent.ROLL_OUT, buttonRollOutHandler, false, 0, true); this.addChild(button); } return button; } /** * @private * * Saves the buttons from the last redraw so that they may be reused. */ protected function createCache():void { this._cachedButtons = this.buttons.concat(); this.buttons = []; } /** * @private * * Removes unneeded buttons that were cached for a redraw. */ protected function clearCache():void { var cacheLength:int = this._cachedButtons.length; for(var i:int = 0; i < cacheLength; i++) { var button:TabButton = this._cachedButtons.pop() as TabButton; button.removeEventListener(Event.CHANGE, buttonChangeHandler); button.removeEventListener(MouseEvent.CLICK, buttonClickHandler); button.removeEventListener(MouseEvent.ROLL_OVER, buttonRollOverHandler); button.removeEventListener(MouseEvent.ROLL_OUT, buttonRollOutHandler); this.removeChild(button); } } /** * @private * Positions and sizes the buttons. */ protected function drawButtons():void { var stylesInvalid:Boolean = this.isInvalid(InvalidationType.STYLES); var rendererStylesInvalid:Boolean = this.isInvalid(InvalidationType.RENDERER_STYLES); var xPosition:Number = 0; var buttonCount:int = this.buttons.length; for(var i:int = 0; i < buttonCount; i++) { var button:Button = Button(this.buttons[i]); button.selected = this._selectedIndex == i; button.enabled = this.enabled; if(i == this._focusIndex) { button.setMouseState("over"); } else { button.setMouseState("up"); } if(stylesInvalid) { this.copyStylesToChild(button, TAB_STYLES); } if(rendererStylesInvalid) { for(var prop:String in this.rendererStyles) { button.setStyle(prop, this.rendererStyles[prop]); } } button.x = xPosition; button.width = NaN; //always auto-size at first button.height = this.height; button.drawNow(); xPosition += button.width; } if(this.autoSizeTabsToTextWidth) { //width changes automatically based on the size of the tabs. this._width = xPosition; } else { //we need to fit the tabs into the specified bounds var totalWidth:Number = xPosition; xPosition = 0; for(i = 0; i < buttonCount; i++) { button = Button(this.buttons[i]); button.x = xPosition; button.width = this.width * (button.width / totalWidth); button.drawNow(); xPosition += button.width; } } if(rendererStylesInvalid) { //clear old renderer styles for(prop in this.rendererStyles) { if(this.rendererStyles[prop] == null) { delete this.rendererStyles[prop]; } } } } //-------------------------------------- // Protected Event Handlers //-------------------------------------- /** * @private * * Requests a redraw when the data provider changes. */ protected function dataChangeHandler(event:DataChangeEvent):void { this.invalidate(InvalidationType.DATA); } /** * @private * Listen for events to allow for keyboard navigation. */ protected function navigationKeyDownHandler(event:KeyboardEvent):void { switch(event.keyCode) { case Keyboard.SPACE: { this.selectedIndex = this.focusIndex; break; } //down and right goes to next tab case Keyboard.DOWN: case Keyboard.RIGHT: var index:int = (this.focusIndex == this.numChildren - 1) ? 0 : (this.focusIndex + 1); if(this.selectionFollowsKeyboardFocus) { this.selectedIndex = index; } else { this.focusIndex = index; } break; //up and left goes to previous tab case Keyboard.UP: case Keyboard.LEFT: index = (this.focusIndex == 0) ? (this.numChildren - 1) : (this.focusIndex - 1); if(this.selectionFollowsKeyboardFocus) { this.selectedIndex = index; } else { this.focusIndex = index; } break; } } /** * @private * Focus the first tab when the TabBar receives focus. */ override protected function focusInHandler(event:FocusEvent):void { this.focusIndex = (this.lastFocusIndex >= 0) ? this.lastFocusIndex : 0; } /** * @private * Save the focus index so that we can resume focus at the same place next time. */ override protected function focusOutHandler(event:FocusEvent):void { this.lastFocusIndex = this.focusIndex; this.focusIndex = -1; } /** * @private * * Captures change events from each button and resets the selected index * to match the last-selected button. */ protected function buttonChangeHandler(event:Event):void { var changedButton:TabButton = event.target as TabButton; if(changedButton.selected) { var index:int = this.buttons.indexOf(changedButton); this.selectedIndex = index; } } /** * @private * * Captures click events from each button, and dispatches the * TabBarEvent.ITEM_CLICK event to listeners. */ protected function buttonClickHandler(event:MouseEvent):void { var button:TabButton = event.currentTarget as TabButton; var index:int = this.buttons.indexOf(button); var item:Object = this._dataProvider.getItemAt(index); this.dispatchEvent(new TabBarEvent(TabBarEvent.ITEM_CLICK, false, false, index, item)); } /** * @private * * Captures roll-over events from each button, and dispatches the * TabBarEvent.ITEM_ROLL_OVER event to listeners. */ protected function buttonRollOverHandler(event:MouseEvent):void { var button:TabButton = event.currentTarget as TabButton; var index:int = this.buttons.indexOf(button); var item:Object = this._dataProvider.getItemAt(index); this.dispatchEvent(new TabBarEvent(TabBarEvent.ITEM_ROLL_OVER, false, false, index, item)); } /** * @private * * Captures roll-out events from each button, and dispatches the * TabBarEvent.ITEM_ROLL_OUT event to listeners. */ protected function buttonRollOutHandler(event:MouseEvent):void { var button:TabButton = event.currentTarget as TabButton; var index:int = this.buttons.indexOf(button); var item:Object = this._dataProvider.getItemAt(index); this.dispatchEvent(new TabBarEvent(TabBarEvent.ITEM_ROLL_OUT, false, false, index, item)); } } }