//////////////////////////////////////////////////////////////////////////////// // // Licensed to the Apache Software Foundation (ASF) under one or more // contributor license agreements. See the NOTICE file distributed with // this work for additional information regarding copyright ownership. // The ASF licenses this file to You under the Apache License, Version 2.0 // (the "License"); you may not use this file except in compliance with // the License. You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// package mx.automation.delegates.advancedDataGrid { import flash.display.DisplayObject; import flash.display.DisplayObjectContainer; import flash.events.Event; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.ui.Keyboard; import flash.utils.getTimer; import mx.automation.Automation; import mx.automation.AutomationHelper; import mx.automation.AutomationIDPart; import mx.automation.IAutomationClass; import mx.automation.IAutomationManager; import mx.automation.IAutomationObject; import mx.automation.IAutomationObjectHelper; import mx.automation.IAutomationTabularData; import mx.automation.delegates.DragManagerAutomationImpl; import mx.automation.delegates.core.ScrollControlBaseAutomationImpl; import mx.automation.events.AutomationDragEvent; import mx.automation.events.AutomationRecordEvent; import mx.automation.events.ListItemSelectEvent; import mx.automation.tabularData.AdvancedListBaseTabularData; import mx.controls.listClasses.AdvancedListBase; import mx.controls.listClasses.IListItemRenderer; import mx.core.EventPriority; import mx.core.mx_internal; import mx.events.DragEvent; import mx.events.ListEvent; import mx.events.ScrollEvent; import mx.events.ScrollEventDirection; import mx.managers.DragManager; import mx.utils.StringUtil; use namespace mx_internal; [Mixin] /** * * Defines methods and properties required to perform instrumentation for the * AdvancedListBase class. * * @see mx.controls.listClasses.AdvancedListBase * * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public class AdvancedListBaseAutomationImpl extends ScrollControlBaseAutomationImpl { include "../../../core/Version.as"; //-------------------------------------------------------------------------- // // Class methods // //-------------------------------------------------------------------------- /** * Registers the delegate class for a component class with automation manager. * * @param root The SystemManger of the application. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public static function init(root:DisplayObject):void { Automation.registerDelegateClass(AdvancedListBase, AdvancedListBaseAutomationImpl); } //-------------------------------------------------------------------------- // // Constructor // //-------------------------------------------------------------------------- /** * Constructor. * @param obj AdvancedListBase object to be automated. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ public function AdvancedListBaseAutomationImpl(obj:AdvancedListBase) { super(obj); //for item renderers already present updateItemRenderers(); obj.addEventListener(Event.ADDED, childAddedHandler, false, 0, true); obj.addEventListener(MouseEvent.CLICK, mouseClickHandler, false, 0, true); obj.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, false, 0, true); obj.addEventListener(ListEvent.ITEM_DOUBLE_CLICK, recordAutomatableEvent, false, 0, true); obj.addEventListener(DragEvent.DRAG_START, dragStartHandler); obj.addEventListener(DragEvent.DRAG_DROP, dragDropHandler, false, EventPriority.DEFAULT+1, true); obj.addEventListener(DragEvent.DRAG_COMPLETE, dragCompleteHandler); obj.addEventListener(AutomationRecordEvent.RECORD, recordHandler); obj.addEventListener(ScrollEvent.SCROLL, scrollHandler); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * @private * storage for the owner component */ // kept the same name so that the implementation of // other methods need not be changed protected function get listBase():AdvancedListBase { return uiComponent as AdvancedListBase; } /** * @private */ protected var preventDragDropRecording:Boolean = true; /** * @private */ protected var shiftKeyDown:Boolean = false; /** * @private */ protected var ctrlKeyDown:Boolean = false; /** * @private */ protected var itemUnderMouse:IListItemRenderer; /** * @private */ protected var dragScrollEvent:ScrollEvent = null; //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @private */ protected function recordListItemSelectEvent(item:IListItemRenderer, trigger:Event, cacheable:Boolean=true):void { var selected:Boolean = listBase.isItemSelected(item.data); var selectionType:String = ListItemSelectEvent.SELECT; var keyEvent:KeyboardEvent = trigger as KeyboardEvent; var mouseEvent:MouseEvent = trigger as MouseEvent; var indexSelection:Boolean = false; if (!Automation.automationManager || !Automation.automationManager.automationEnvironment || !Automation.automationManager.recording) return ; var automationClass:IAutomationClass = Automation.automationManager.automationEnvironment.getAutomationClassByInstance(listBase); if (automationClass) { var propertyNameMap:Object = automationClass.propertyNameMap; if (propertyNameMap["enableIndexBasedSelection"]) { selectionType = ListItemSelectEvent.SELECT_INDEX; indexSelection = true; } } var event:ListItemSelectEvent = new ListItemSelectEvent(selectionType); if (indexSelection) fillItemRendererIndex(item, event); else event.itemRenderer = item; event.triggerEvent = trigger; if (keyEvent) { event.ctrlKey = keyEvent.ctrlKey; event.shiftKey = keyEvent.shiftKey; event.altKey = keyEvent.altKey; } else if (mouseEvent) { event.ctrlKey = mouseEvent.ctrlKey; event.shiftKey = mouseEvent.shiftKey; event.altKey = mouseEvent.altKey; } recordAutomatableEvent(event, cacheable); } //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- /** * @private */ mx_internal var itemAutomationNameFunction:Function = getItemAutomationValue; /** * @private */ public function getItemAutomationValue(item:IAutomationObject):String { // check for atleast one non-null item var values:Array = item.automationValue; if (values) { var itemCount:int = values.length; for (var i:int = 0; i < itemCount; ++i) { if (values[i]) return values.join(" | "); // found one non null item, so return } } return null; } /** * @private */ public function getItemAutomationName(item:IAutomationObject):String { return item.automationName; } /** * @private */ public function getItemAutomationIndex(item:IAutomationObject):String { return String("index:" + listBase.itemRendererToIndex(IListItemRenderer(item))); } //-------------------------------------------------------------------------- // // Overridden properties // //-------------------------------------------------------------------------- //---------------------------------- // automationValue //---------------------------------- /** * @private */ override public function get automationValue():Array { var result:Array = []; var selectedItemsAboveView:Boolean = false; var selectedItemsBelowView:Boolean = false; var selItems:Array = listBase.selectedIndices; var n:int = selItems.length; var listLength:int = listBase.rendererArray.length * listBase.rendererArray[0].length; for (var i:int = 0; i < n; i++) { var viewRelativeIndex:int = selItems[i] - listBase.verticalScrollPosition; if (viewRelativeIndex < 0) { selectedItemsAboveView = true; } else if (viewRelativeIndex >= listLength) { selectedItemsBelowView = true; } else { var item:IListItemRenderer = listBase.indexToItemRenderer(selItems[i]); if(item is IAutomationObject) result.push(IAutomationObject(item).automationValue); } } if (selectedItemsAboveView) result.unshift("..."); if (selectedItemsBelowView) result.push("..."); return result; } /** * @private */ override public function createAutomationIDPart(child:IAutomationObject):Object { var help:IAutomationObjectHelper = Automation.automationObjectHelper; return (help ? help.helpCreateIDPart(uiAutomationObject, child, itemAutomationNameFunction, getItemAutomationIndex) : null); } override public function createAutomationIDPartWithRequiredProperties(child:IAutomationObject, properties:Array):Object { var help:IAutomationObjectHelper = Automation.automationObjectHelper; return (help ? help.helpCreateIDPartWithRequiredProperties(uiAutomationObject, child,properties, itemAutomationNameFunction, getItemAutomationIndex) : null); } /** * @private */ override public function resolveAutomationIDPart(part:Object):Array { var help:IAutomationObjectHelper = Automation.automationObjectHelper; return help ? help.helpResolveIDPart(uiAutomationObject, part) : null; } /** * @private */ override public function get numAutomationChildren():int { var listItems:Array = listBase.rendererArray; if (listItems.length == 0) return 0; var result:int = listItems.length * listItems[0].length; var row:uint = listItems.length - 1; var col:uint = listItems[0].length - 1; // now we traverse in the list from the end till we are done // with the empty cells. (i.e we need to remove the empty element count // from the total count. while (!listItems[row][col] && result > 0) { result--; if (col != 0) col--; else if (row != 0) { row--; col = listItems[0].length - 1; } } return result; } /** * @private */ override public function getAutomationChildren():Array { var childrenList:Array = new Array(); var listItems:Array = listBase.rendererArray; // we get this as the 2 dim array of row and columns // we need to make this as single element array //while (!listItems[row][col] var rowcount:int = listItems?listItems.length:0; if (rowcount != 0) { var coulumcount:int = 0; if ((listItems[0]) is Array) coulumcount = (listItems[0] as Array).length; for (var i:int = 0; i < rowcount ; i++) { for (var j:int = 0; j < coulumcount ; j++) { var item:IListItemRenderer = listItems[i][j]; if(item) childrenList.push(item as IAutomationObject); } } } return childrenList; } /** * @private */ override public function getAutomationChildAt(index:int):IAutomationObject { var listItems:Array = listBase.rendererArray; var numCols:int = listItems[0].length; var row:uint = uint(numCols == 0 ? 0 : index / numCols); var col:uint = uint(numCols == 0 ? index : index % numCols); var item:IListItemRenderer = listItems[row][col]; return item as IAutomationObject; } /** * A matrix of the automationValues of each item in the grid. The return value * is an array of rows, each of which is an array of item renderers (row-major). * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 3 */ override public function get automationTabularData():Object { return new AdvancedListBaseTabularData(listBase); } /** * @private */ override public function replayAutomatableEvent(event:Event):Boolean { var help:IAutomationObjectHelper = Automation.automationObjectHelper; switch (event.type) { case MouseEvent.CLICK: { return help.replayClick(uiComponent, MouseEvent(event)); } case ListEvent.ITEM_DOUBLE_CLICK: { var clickEvent:ListEvent = ListEvent(event); return replayMouseDoubleClickOnItem(clickEvent.itemRenderer); } case KeyboardEvent.KEY_DOWN: { listBase.setFocus(); return help.replayKeyboardEvent(uiComponent, KeyboardEvent(event)); } case ListItemSelectEvent.SELECT_INDEX: case ListItemSelectEvent.DESELECT: case ListItemSelectEvent.MULTI_SELECT: case ListItemSelectEvent.SELECT: { var completeTime:Number = getTimer() + listBase.getStyle("selectionDuration"); help.addSynchronization(function():Boolean { return getTimer() >= completeTime; }); var lise:ListItemSelectEvent = ListItemSelectEvent(event); if (event.type == ListItemSelectEvent.SELECT_INDEX) { listBase.scrollToIndex(lise.itemIndex); lise.itemRenderer = getItemRendererForEvent(lise); } else { if (!lise.itemRenderer) findItemRenderer(lise); } if (!lise.itemRenderer) { // it was observed that during recording at some cases // an empty select was getting recorded. this issue is not // reproducable with known steps. // hence controlling the RTE at the time of replay // this is not the final fix. // we need to figure out the empty select recording scenario return false; } // keyboard and mouse are currently treated the same if (lise.triggerEvent is MouseEvent) { return replayMouseClickOnItem(lise.itemRenderer, lise.ctrlKey, lise.shiftKey, lise.altKey); } else if (lise.triggerEvent is KeyboardEvent) { return help.replayKeyDownKeyUp(lise.itemRenderer, Keyboard.SPACE, lise.ctrlKey, lise.shiftKey, lise.altKey); } else { throw new Error(); } } case AutomationDragEvent.DRAG_START: case AutomationDragEvent.DRAG_DROP: case AutomationDragEvent.DRAG_COMPLETE: { return DragManagerAutomationImpl.replayAutomatableEvent(uiAutomationObject, event); } case ScrollEvent.SCROLL: { if (DragManager.isDragging) { var scrollEv:ScrollEvent = event as ScrollEvent; if (scrollEv.direction == ScrollEventDirection.VERTICAL) { listBase.verticalScrollPosition = scrollEv.position; listBase.validateNow(); } DragManagerAutomationImpl.callBackBeforeDrop = function():void { //stop dragScroll as it modifies the scroll position listBase.resetDragScrolling(); var scrollEv:ScrollEvent = event as ScrollEvent; if (scrollEv.direction == ScrollEventDirection.VERTICAL) { listBase.verticalScrollPosition = scrollEv.position; listBase.validateNow(); } }; return true; } } // fall thru if not dragging while scroll occurs default: { return super.replayAutomatableEvent(event); } } } /** * @private * Plays back MouseEvent.CLICK on the item renderer. */ protected function replayMouseClickOnItem(item:IListItemRenderer, ctrlKey:Boolean = false, shiftKey:Boolean = false, altKey:Boolean = false):Boolean { var me:MouseEvent = new MouseEvent(MouseEvent.CLICK); me.ctrlKey = ctrlKey; me.altKey = altKey; me.shiftKey = shiftKey; var help:IAutomationObjectHelper = Automation.automationObjectHelper; return help.replayClick(item, me); } /** * @private * Plays back MouseEvent.DOUBLE_CLICK on the item renderer. */ protected function replayMouseDoubleClickOnItem(item:IListItemRenderer):Boolean { var me:MouseEvent = new MouseEvent(MouseEvent.DOUBLE_CLICK); var help:IAutomationObjectHelper = Automation.automationObjectHelper; return help.replayMouseEvent(item, me); } /** * @private */ protected function updateItemRenderers():void { var listItems:Array = listBase.rendererArray; if (listItems.length == 0) return ; var rows:uint = listItems.length; var cols:uint = listItems[0].length; var ownerList:DisplayObjectContainer = listBase as DisplayObjectContainer; for (var i:int = 0; i < rows; ++i) { for (var j:int = 0; j < cols; ++j) { var item:IListItemRenderer = listItems[i][j]; if (item) { item.owner = ownerList; if(item is IAutomationObject) IAutomationObject(item).showInAutomationHierarchy = true; } } } } //-------------------------------------------------------------------------- // // Event handlers // //-------------------------------------------------------------------------- /** * @private */ public function childAddedHandler(event:Event):void { var child:Object = event.target; if (child is IListItemRenderer && child.parent == listBase.getListContentHolder()) { IListItemRenderer(child).owner = uiComponent as DisplayObjectContainer; if(child is IAutomationObject) child.showInAutomationHierarchy = true; } } /** * @private */ protected function mouseClickHandler(event:MouseEvent):void { var item:IListItemRenderer = listBase.getItemRendererForMouseEvent(event); if (!item) { //DataGrid overrides displayObjectToItemRenderer to return //null if the item is the active item editor, so that's //not a reliable way of determining if the user clicked on a blank //row or now, so use mouseEventToItemRendererOrEditor instead if (listBase.mouseEventToItemRendererOrEditor(event) == null) recordAutomatableEvent(event, true); return; } else { // take the key modifiers from the mouseDown event because // they were used by List for making the selection event.ctrlKey = ctrlKeyDown; event.shiftKey = shiftKeyDown; recordListItemSelectEvent(item, event); } } /** * @private */ protected function mouseDownHandler(event:MouseEvent):void { ctrlKeyDown = event.ctrlKey; shiftKeyDown = event.shiftKey; itemUnderMouse = listBase.getItemRendererForMouseEvent(event); } /** * @private */ override protected function keyDownHandler(event:KeyboardEvent):void { if (event.keyCode == Keyboard.SPACE) { var listItems:Array = listBase.rendererArray; var caretIndex:int = listBase.getCaretIndex(); if (caretIndex != -1) { var rendererIndex:int = caretIndex - listBase.verticalScrollPosition; // + listBase.lockedRowCount; // fix to adapt to the new listbase (which does not have lockedRowCount var item:IListItemRenderer = listItems[rendererIndex][0] as IListItemRenderer; recordListItemSelectEvent(item, event); } } else if (event.keyCode != Keyboard.SPACE && event.keyCode != Keyboard.CONTROL && event.keyCode != Keyboard.SHIFT && event.keyCode != Keyboard.TAB) recordAutomatableEvent(event); } /** * @private */ protected function dragStartHandler(event:DragEvent):void { var shouldEventBeDispatched:Boolean = true; // air gets the drag start event twice. we need to record only once // so we are doing it based on the dragSource // first we need to check whether the env is air or not. // for automation we have a special class to handle the if (AutomationHelper.isCurrentAppAir()) { // for air we need to send the event only if the dragSource is present. // drag start happens from the list base (mouse down + mouse move) // and from the listener of the native drag start. // we need the details from the native event, hence handle it only // if it is from the native drag event. if (!event.dragSource) shouldEventBeDispatched = false; } if (shouldEventBeDispatched) { var drag:AutomationDragEvent = new AutomationDragEvent(event.type); drag.draggedItem = itemUnderMouse as IAutomationObject; drag.ctrlKey = ctrlKeyDown; drag.shiftKey = shiftKeyDown; var re:AutomationRecordEvent = new AutomationRecordEvent(AutomationRecordEvent.RECORD, false); re.automationObject = uiAutomationObject; re.cacheable = false; re.replayableEvent = drag; var am:IAutomationManager = Automation.automationManager; preventDragDropRecording = false; am.recordAutomatableEvent(uiAutomationObject, re); preventDragDropRecording = true; } } /** * @private */ protected function dragDropHandler(event:DragEvent):void { if (dragScrollEvent) { recordAutomatableEvent(dragScrollEvent); dragScrollEvent=null; } var drag:AutomationDragEvent = new AutomationDragEvent(event.type); drag.action = event.action; var index:int = listBase.calculateDropIndex(event); if (listBase.dataProvider && index != listBase.dataProvider.length) { // fix to adapt to the new listbase (which does not have lockedRowCount // if (index >= listBase.lockedRowCount) if (index >= 0) index -= listBase.verticalScrollPosition; var rc:Number = listBase.rendererArray.length; if (index >= rc) index = rc - 1; if (index < 0) index = 0; drag.draggedItem = getAutomationChildAt(index); } var am:IAutomationManager = Automation.automationManager; preventDragDropRecording = false; am.recordAutomatableEvent(uiAutomationObject, drag); preventDragDropRecording = true; } /** * @private */ protected function dragCompleteHandler(event:DragEvent):void { if (event.action == DragManager.NONE) { var drag:AutomationDragEvent = new AutomationDragEvent(event.type); drag.action = event.action; var am:IAutomationManager = Automation.automationManager; preventDragDropRecording = false; am.recordAutomatableEvent(uiAutomationObject, drag); preventDragDropRecording = true; } } /** * @private */ private function recordHandler(ev:AutomationRecordEvent):void { // list based controls handle drag-drop on their own if (preventDragDropRecording && ev.replayableEvent is AutomationDragEvent) ev.preventDefault(); } /** * @private */ private function scrollHandler(ev:ScrollEvent):void { // normall scrolling is recorded by the base class // auto scrolling during drag is recorded here if (DragManager.isDragging) dragScrollEvent = ev; } /** * @private */ protected function trimArray(val:Array):void { var n:int = val.length; for (var i:int = 0; i < n; i++) { val[i] = StringUtil.trim(val[i]); } } /** * @private */ protected function findItemRenderer(selectEvent:ListItemSelectEvent):Boolean { if (selectEvent.itemAutomationValue && selectEvent.itemAutomationValue.length) { var itemLabel:String = selectEvent.itemAutomationValue; var tabularData:IAutomationTabularData = automationTabularData as IAutomationTabularData; var values:Array = tabularData.getValues(0, tabularData.numRows); var length:int = values.length; var part:AutomationIDPart = new AutomationIDPart(); part.automationName = itemLabel; var labels:Array = itemLabel.split("|"); trimArray(labels); var index:int = 0; for each (var a:Array in values) { values[index] = []; trimArray(a); var colIndex:int = 0 ; for each (var b:String in a) { var splitArray:Array = b.split("|"); for each ( var c:String in splitArray) { values[index].push(c); } } trimArray(values[index]); ++index; } var n:int = labels.length; for (index = 0; index < n; ++index) { var lString:String = labels[index]; if (lString.charAt(0) == "*" && lString.charAt(lString.length-1) == "*") labels[index] = lString.substr(1, lString.length-2); } for (index = 0; index < length; ++index) { if(compare(labels, values[index])) { listBase.scrollToIndex(index); var ao:IAutomationObject = Automation.automationManager.resolveIDPartToSingleObject(uiAutomationObject, part); if (ao) { selectEvent.itemRenderer = ao as IListItemRenderer; return true; } } } } return false; } /** * @private */ protected function compare(labels:Array, values:Array):Boolean { if (labels.length != values.length) return false; var length:int = labels.length; for ( var index:int = 0; index < length; ++index) { if (labels[index] != values[index]) return false; } return true; } /** * @private */ protected function getItemRendererForEvent(lise:ListItemSelectEvent):IListItemRenderer { return listBase.indexToItemRenderer(lise.itemIndex); } /** * @private */ protected function fillItemRendererIndex(item:IListItemRenderer, event:ListItemSelectEvent):void { event.itemIndex = listBase.itemRendererToIndex(item); } /** * @private */ public function getItemsCount():int { if (listBase.dataProvider) return listBase.dataProvider.length; return 0; } } }