//////////////////////////////////////////////////////////////////////////////// // // 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 { import flash.display.DisplayObject; import flash.display.DisplayObjectContainer; import flash.display.InteractiveObject; import flash.display.Shape; import flash.events.Event; import flash.events.MouseEvent; import flash.events.TimerEvent; import flash.filesystem.File; import flash.filesystem.FileMode; import flash.filesystem.FileStream; import flash.geom.Point; import flash.system.Capabilities; import flash.text.TextField; import flash.utils.Timer; import mx.core.IVisualElement; import mx.core.IVisualElementContainer; import mx.core.mx_internal; import mx.graphics.SolidColor; import mx.graphics.SolidColorStroke; import mx.utils.GetTimerUtil; import spark.components.Group; import spark.components.Scroller; import spark.core.SpriteVisualElement; import spark.primitives.Ellipse; import spark.primitives.Path; use namespace mx_internal; /** * Exposes some helper methods that are useful for testing touch scrolling * on a mobile device. These methods can be used in a standalone application * to reproduce or demonstrate a bug and is also used in the SimulateMouseGesture * Mustella test step. * * Example usage 1 - recording events: * * * * * * * * Example usage 2 - playing back events: * * * * * */ public class TouchScrollingUtil { /** * The name of the event that is fired when all mouse events have been fired */ public static const SIMULATION_COMPLETE:String = "simulationComplete"; /** * Keeps track of all the mouse events that have fired while tracking was enabled */ public static var recordedMouseEvents:Array = new Array(); /** * Controls whether to trace out extra debug information, for example tracing * out information about every mouse event as it is dispatched. */ public static var enableVerboseTraceOuput:Boolean = true; /** * Dispatches a series of MouseEvents that simulate how a user scrolls * using touch scrolling on a mobile device. * * @param actualTarget - the target component to scroll * * @param events - An array of mouse events to dispatch. * * Use this for realistic simulation as you have complete control over the * type, location, and time of each event. * * These events should be defined as an array of objects in this form: * * * * ... * * * If this property is not null then it takes precedence over any * dragX/dragY values that might also be defined. * * TODO: Possible Enhancement: Allow each individual mouse event entry to specify its own * waitEvent before continuing on to the next. This would allow for more control * over when the events are fired, but may not be of any value. * * TODO: Possible Enhancement: Fully support sequences that go outside the bounds of the target. * * @param dragXFrom - the x coordinate of the target to start the drag motion * @param dragYFrom - the y coordinate of the target to start the drag motion * @param dragXTo - the x coordinate of the target to end the drag motion * @param dragYTo - the y coordinate of the target to end the drag motion * @param delay - the time between events when an events array isn't defined * */ public static function simulateTouchScroll(actualTarget:Object, events:Array, recordedDPI:Number = NaN, dragXFrom:Number = NaN, dragYFrom:Number = NaN, dragXTo:Number = NaN, dragYTo:Number = NaN, delay:Number = 17):void { // reset the index into the event list var eventIndex:int = 0; // if a specific event sequence wasn't provided, then create one if (events == null) events = createEventsArray(dragXFrom, dragYFrom, dragXTo, dragYTo, delay); // shove the target into each element of the events array for (var j:int = 0; j < events.length; j++) events[j].target = actualTarget; // setup the timer based firing var eventTimer:Timer = new Timer(1); // the method that loops through the events to fire var tickerFunction:Function = function(e:TimerEvent):void { if (eventIndex >= events.length) { // all mouse events have been fired at this point // turn off fake time GetTimerUtil.fakeTimeValue = undefined; // turn mouse event thinning back on Scroller.dragEventThinning = true; // signal that this test step is ready for completion actualTarget.dispatchEvent(new Event(SIMULATION_COMPLETE)); // stop the timer loop eventTimer.stop(); eventTimer.removeEventListener(TimerEvent.TIMER, tickerFunction); return; } // trace details on the event we are firing traceLog("Dispatching MouseEvent:", events[eventIndex].type, events[eventIndex].localX, events[eventIndex].localY, events[eventIndex].fakeTimeValue); // update the fake time GetTimerUtil.fakeTimeValue = events[eventIndex].fakeTimeValue; // turn off mouse event thinning // ---- // Late in the 4.5 release the runtime changed their behavior on Android that // fired too many mouseMove events making Flex sluggish on drag scrolls. We // put logic in the SDK to thin out excess events and this logic is // non-deterministic so we need to turn that logic off in Mustella. // ---- // TODO: This is a dependency on the SDK and a possible risk for automation // not following the same code path as a user. // We're stuck with it for now until the runtime allows us more control // over the mouse event firing rate. // ---- // See http://bugs.adobe.com/jira/browse/SDK-29188 for a full explanation // Scroller.dragEventThinning = false; // scale the localX/localY co-ordinates if requested var adjustedLocalX:Number = scaleByDPIRatio(events[eventIndex].localX, recordedDPI); var adjustedLocalY:Number = scaleByDPIRatio(events[eventIndex].localY, recordedDPI); // fire the next event dispatchMouseEvent(events[eventIndex].target, events[eventIndex].type, adjustedLocalX, adjustedLocalY); // update the timer delay to be the difference between time values of the next and current events if (eventIndex + 1 < events.length) { var nextTimeValue:int = events[eventIndex + 1].fakeTimeValue; var currTimeValue:int = events[eventIndex].fakeTimeValue; eventTimer.delay = (nextTimeValue - currTimeValue); traceLog('timer is now', eventTimer.delay); } // move on to the next event eventIndex++; }; // start firing the mouse events eventTimer.addEventListener(TimerEvent.TIMER, tickerFunction); eventTimer.start(); } /** * Similar to simulateTouchScroll(), but used in Mustella so it's not based * on a timer but rather fires off a couple mouse events per enterFrame. * * @see simulateTouchScroll() */ public static function simulateTouchScrollFrameBased(actualTarget:Object, events:Array, recordedDPI:Number = NaN, dragXFrom:Number = NaN, dragYFrom:Number = NaN, dragXTo:Number = NaN, dragYTo:Number = NaN, delay:Number = 17):void { // reset the index into the event list var eventIndex:int = 0; // if a specific event sequence wasn't provided, then create one if (events == null) events = createEventsArray(dragXFrom, dragYFrom, dragXTo, dragYTo, delay); // shove the target into each element of the events array for (var j:int = 0; j < events.length; j++) events[j].target = actualTarget; // the method that loops through the events to fire var tickerFunction:Function = function(e:Event):void { // fire a few mouse events per enterFrame var numEventsPerEnterFrame:int = 3; for (var i:int = 0; i < numEventsPerEnterFrame; i++) { if (eventIndex >= events.length) { // all mouse events have been fired at this point // turn off fake time GetTimerUtil.fakeTimeValue = undefined; // turn mouse event thinning back on Scroller.dragEventThinning = true; // signal that this test step is ready for completion actualTarget.dispatchEvent(new Event(SIMULATION_COMPLETE)); // remove the enterFrame listener actualTarget.removeEventListener("enterFrame", tickerFunction) return; } // trace details on the event we are firing traceLog("Dispatching MouseEvent:", events[eventIndex].type, events[eventIndex].localX, events[eventIndex].localY, events[eventIndex].fakeTimeValue); // update the fake time GetTimerUtil.fakeTimeValue = events[eventIndex].fakeTimeValue; // turn off mouse event thinning // ---- // Late in the 4.5 release the runtime changed their behavior on Android that // fired too many mouseMove events making Flex sluggish on drag scrolls. We // put logic in the SDK to thin out excess events and this logic is // non-deterministic so we need to turn that logic off in Mustella. // ---- // TODO: This is a dependency on the SDK and a possible risk for automation // not following the same code path as a user. // We're stuck with it for now until the runtime allows us more control // over the mouse event firing rate. // ---- // See http://bugs.adobe.com/jira/browse/SDK-29188 for a full explanation // Scroller.dragEventThinning = false; // scale the localX/localY co-ordinates if requested var adjustedLocalX:Number = scaleByDPIRatio(events[eventIndex].localX, recordedDPI); var adjustedLocalY:Number = scaleByDPIRatio(events[eventIndex].localY, recordedDPI); trace(adjustedLocalX, adjustedLocalY); // fire the next event dispatchMouseEvent(events[eventIndex].target, events[eventIndex].type, adjustedLocalX, adjustedLocalY); // move on to the next event eventIndex++; } } // start firing the mouse events actualTarget.addEventListener("enterFrame", tickerFunction); } /** * This takes an x/y value and scales it by the current device's exact DPI over the recorded * device's exact dpi. * * This allows support for recording a mouseEvent sequence at one DPI and have it scale automatically * on different DPIs to maintain the same physical distance scrolled (in inches). */ private static function scaleByDPIRatio(value:Number, recordedDPI:Number):Number { if (!isNaN(recordedDPI)) return Math.round(value * (Capabilities.screenDPI / recordedDPI)); else return value; } /** * TODO: This is an untyped version of MouseEventEntry in Mustella. */ private static function createMouseEventEntry(type:String, localX:Number, localY:Number, fakeTimeValue:Number):Object { var mouseEventEntry:Object = new Object(); mouseEventEntry.type = type; mouseEventEntry.localX = localX; mouseEventEntry.localY = localY; mouseEventEntry.fakeTimeValue = fakeTimeValue; return mouseEventEntry; } /** * Creates a sequence of mouse events to perform the requested drag scroll */ private static function createEventsArray(dragXFrom:Number, dragYFrom:Number, dragXTo:Number, dragYTo:Number, delay:Number):Array { var arr:Array = new Array(); var deltaX:Number = isNaN(dragXFrom - dragXTo) ? 0 : dragXFrom - dragXTo; var deltaY:Number = isNaN(dragYFrom - dragYTo) ? 0 : dragYFrom - dragYTo; var numSteps:Number = 6; var chunkX:Number = deltaX / numSteps; var chunkY:Number = deltaY / numSteps; // first need a move, rollOver, over, mouseDown at the start position arr.push(createMouseEventEntry(MouseEvent.MOUSE_MOVE, dragXFrom, dragYFrom, arr.length * delay)); arr.push(createMouseEventEntry(MouseEvent.ROLL_OVER, dragXFrom, dragYFrom, arr.length * delay)); arr.push(createMouseEventEntry(MouseEvent.MOUSE_OVER, dragXFrom, dragYFrom, arr.length * delay)); arr.push(createMouseEventEntry(MouseEvent.MOUSE_DOWN, dragXFrom, dragYFrom, arr.length * delay)); for (var i:int = 0; i < numSteps; i++) { // then a couple mouseMoves along the way arr.push(createMouseEventEntry(MouseEvent.MOUSE_MOVE, Math.round(dragXFrom - (chunkX * i)), Math.round(dragYFrom - (chunkY * i)), arr.length * delay)); } // then a few mouseMoves near/at the end to take away any potential // velocity so we guarantee a drag instead of a throw arr.push(createMouseEventEntry(MouseEvent.MOUSE_MOVE, dragXTo, dragYTo, arr.length * delay)); arr.push(createMouseEventEntry(MouseEvent.MOUSE_MOVE, dragXTo, dragYTo, arr.length * delay)); arr.push(createMouseEventEntry(MouseEvent.MOUSE_MOVE, dragXTo, dragYTo, arr.length * delay)); arr.push(createMouseEventEntry(MouseEvent.MOUSE_MOVE, dragXTo, dragYTo, arr.length * delay)); // then a mouseMove at the destination arr.push(createMouseEventEntry(MouseEvent.MOUSE_MOVE, dragXTo, dragYTo, arr.length * delay)); // finally a mouseUp at the destination arr.push(createMouseEventEntry(MouseEvent.MOUSE_UP, dragXTo, dragYTo, arr.length * delay)); return arr; } /** * Fires a mouseEvent of the given type and location on the target. * * This method inspired by DispatchMouseClickEvent and tweaked to be * a static method available outside of Mustella too. This means a few * more optional parameters. */ public static function dispatchMouseEvent(actualTarget:Object, type:String, localX:Number = NaN, localY:Number = NaN, stageX:Number = NaN, stageY:Number = NaN, ctrlKey:Boolean = false, shiftKey:Boolean = false, delta:Number = 0, relatedObject:Object = null):void { var event:MouseEvent = new MouseEvent(type, true); // all mouse events bubble event.ctrlKey = ctrlKey; event.shiftKey = shiftKey; event.buttonDown = type == "mouseDown"; event.delta = delta; if (relatedObject && relatedObject.length > 0) { // SEJS: Removing Mustella specific context stuff for now so this static method // is available outside of Mustella too. TODO: Look into refactoring to allow this support // //event.relatedObject = InteractiveObject(context.stringToObject(relatedObject)); } var stagePt:Point; if (!isNaN(localX) && !isNaN(localY)) { stagePt = actualTarget.localToGlobal(new Point(localX, localY)); } else if (!isNaN(stageX) && !isNaN(stageY)) { stagePt = new Point(stageX, stageY); } else { stagePt = actualTarget.localToGlobal(new Point(0, 0)); } // SEJS: Removing Mustella specific context stuff for now so this static method // is available outside of Mustella too. TODO: Look into refactoring to allow this support // // This class was inspired by DispatchMouseClickEvent, but in a mobile sense we don't care // about other sandboxes for now so we can hopefully remove this mustella context sensitive code // //root[mouseX] = stagePt.x; //root[mouseY] = stagePt.y; //UnitTester.setMouseXY(stagePt); // //if (root["topLevelSystemManager"] != root) //{ // root["topLevelSystemManager"][mouseX] = stagePt.x; // root["topLevelSystemManager"][mouseY] = stagePt.y; //} if (actualTarget is DisplayObjectContainer) { var targets:Array = actualTarget.stage.getObjectsUnderPoint(stagePt); // SEJS: Removing Mustella specific context stuff for now so this static method // is available outside of Mustella too. TODO: Look into refactoring to allow this support // // This class was inspired by DispatchMouseClickEvent, but in a mobile sense we don't care // about other sandboxes for now so we can hopefully remove this mustella context sensitive code // //var arr:Array = UnitTester.getObjectsUnderPoint(DisplayObject(actualTarget), stagePt); //targets = targets.concat(arr); for (var i:int = targets.length - 1; i >= 0; i--) { if (targets[i] is InteractiveObject) { if (targets[i] is TextField && !targets[i].selectable) { actualTarget = targets[i].parent; break; } if (InteractiveObject(targets[i]).mouseEnabled) { actualTarget = targets[i]; break; } } else if (targets[i] is Shape) { // Looks like getObjectsUnderPoint() returns a Shape for FXG elements. // Here we assume that if we see a Shape like this then we check its parent // DisplayObjectContainer to see if it has mouseEnabled true // FIXME: Talk to Brian and Alex to see if this is the right fix and get it into DispatchMouseEvent/DispatchMouseClickEvent and here. is this a player bug? var shapeParent:DisplayObjectContainer = (targets[i] as Shape).parent; if (shapeParent.mouseEnabled) { actualTarget = targets[i]; break; } } else { try { actualTarget = targets[i].parent; while (actualTarget) { if (actualTarget is InteractiveObject) { if (InteractiveObject(actualTarget).mouseEnabled) { break; } } actualTarget = actualTarget.parent; } if (actualTarget) break; } catch (e:Error) { trace('error'); if (actualTarget) break; } } } } // Examine parent chain for "mouseChildren" set to false: try { var parent:DisplayObjectContainer = actualTarget.parent; while (parent) { if (!parent.mouseChildren){ trace('mouseChildren step: set actualTarget to parent:', parent); actualTarget = parent; } parent = parent.parent; } } catch (e1:Error) { } var localPt:Point = actualTarget.globalToLocal(stagePt); event.localX = localPt.x; event.localY = localPt.y; if (actualTarget is TextField) { if (type == "mouseDown") { var charIndex:int = actualTarget.getCharIndexAtPoint(event.localX, event.localY); actualTarget.setSelection(charIndex + 1, charIndex + 1); } } try { actualTarget.dispatchEvent(event); } catch (e2:Error) { trace("Error: Exception thrown in TouchScrollingUtil.dispatchMouseEvent()"); // SEJS: Removing Mustella specific context stuff for now so this static method // is available outside of Mustella too. TODO: Look into refactoring to allow this support // //TestOutput.logResult("Exception thrown in DispatchMouseClickEvent."); //testResult.doFail (e2.getStackTrace()); return; } } /** placeholder for the function closure so the listener can be removed later */ private static var traceFunctionHolder:Function = null; /** placeholder for the function closure so the listener can be removed later */ private static var enterFunctionHolder:Function = null; /** * Traces all of the relevant mouse events that happen on the target. You can access an array of * these events via the recordedMoueEvents static property. * * @param target - the component to track mouse events on * @param showEnterFrameEvents - set this to true to show when enterFrames are being fired within the sequence */ public static function enableMouseEventTracking(target:DisplayObject, showEnterFrameEvents:Boolean = false):void { // closure for tracing mouse events var traceMouseEvents:Function = function (event:MouseEvent):void { // convert the stage value to be relative to the main target var stagePt:Point = event.target.localToGlobal(new Point(event.localX, event.localY)); var targetPt:Point = target.globalToLocal(stagePt); var newEvent:Object = new Object(); newEvent.target = target; newEvent.type = event.type; newEvent.localX = targetPt.x; newEvent.localY = targetPt.y; newEvent.fakeTimeValue = flash.utils.getTimer(); // track this event recordedMouseEvents.push(newEvent); // trace the event trace(newEvent.type, newEvent.localX, newEvent.localY, newEvent.fakeTimeValue); } // closure for tracing enterFrame events var traceEnterFrameEvents:Function = function (event:Event):void { if (showEnterFrameEvents) trace(event.type); } // throw the closures into the static placeholders so they can be removed later traceFunctionHolder = traceMouseEvents; enterFunctionHolder = traceEnterFrameEvents; // listen to enterFrame and all mouse events (use a higher priority than Scroller) // TODO: Containers seem to need a different useCapture value than elements if (target is IVisualElementContainer) { target.addEventListener(MouseEvent.CLICK, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.CONTEXT_MENU, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.DOUBLE_CLICK, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.MIDDLE_CLICK, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.MIDDLE_MOUSE_DOWN, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.MIDDLE_MOUSE_UP, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.MOUSE_DOWN, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.MOUSE_MOVE, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.MOUSE_OUT, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.MOUSE_OVER, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.MOUSE_UP, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.MOUSE_WHEEL, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.RIGHT_CLICK, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.RIGHT_MOUSE_DOWN, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.RIGHT_MOUSE_UP, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.ROLL_OUT, traceFunctionHolder, true, 1); target.addEventListener(MouseEvent.ROLL_OVER, traceFunctionHolder, true, 1); } else { target.addEventListener(MouseEvent.CLICK, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.CONTEXT_MENU, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.DOUBLE_CLICK, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.MIDDLE_CLICK, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.MIDDLE_MOUSE_DOWN, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.MIDDLE_MOUSE_UP, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.MOUSE_DOWN, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.MOUSE_MOVE, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.MOUSE_OUT, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.MOUSE_OVER, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.MOUSE_UP, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.MOUSE_WHEEL, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.RIGHT_CLICK, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.RIGHT_MOUSE_DOWN, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.RIGHT_MOUSE_UP, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.ROLL_OUT, traceFunctionHolder, false, 1); target.addEventListener(MouseEvent.ROLL_OVER, traceFunctionHolder, false, 1); } target.addEventListener(Event.ENTER_FRAME, enterFunctionHolder); } /** * Removes the mouse and enterFrame event listeners from the target. */ public static function disableMouseEventTracking(target:DisplayObject):void { if (target is IVisualElementContainer) { target.removeEventListener(MouseEvent.CLICK, traceFunctionHolder, true); target.removeEventListener(MouseEvent.CONTEXT_MENU, traceFunctionHolder, true); target.removeEventListener(MouseEvent.DOUBLE_CLICK, traceFunctionHolder, true); target.removeEventListener(MouseEvent.MIDDLE_CLICK, traceFunctionHolder, true); target.removeEventListener(MouseEvent.MIDDLE_MOUSE_DOWN, traceFunctionHolder, true); target.removeEventListener(MouseEvent.MIDDLE_MOUSE_UP, traceFunctionHolder, true); target.removeEventListener(MouseEvent.MOUSE_DOWN, traceFunctionHolder, true); target.removeEventListener(MouseEvent.MOUSE_MOVE, traceFunctionHolder, true); target.removeEventListener(MouseEvent.MOUSE_OUT, traceFunctionHolder, true); target.removeEventListener(MouseEvent.MOUSE_OVER, traceFunctionHolder, true); target.removeEventListener(MouseEvent.MOUSE_UP, traceFunctionHolder, true); target.removeEventListener(MouseEvent.MOUSE_WHEEL, traceFunctionHolder, true); target.removeEventListener(MouseEvent.RIGHT_CLICK, traceFunctionHolder, true); target.removeEventListener(MouseEvent.RIGHT_MOUSE_DOWN, traceFunctionHolder, true); target.removeEventListener(MouseEvent.RIGHT_MOUSE_UP, traceFunctionHolder, true); target.removeEventListener(MouseEvent.ROLL_OUT, traceFunctionHolder, true); target.removeEventListener(MouseEvent.ROLL_OVER, traceFunctionHolder, true); } else { target.removeEventListener(MouseEvent.CLICK, traceFunctionHolder, false); target.removeEventListener(MouseEvent.CONTEXT_MENU, traceFunctionHolder, false); target.removeEventListener(MouseEvent.DOUBLE_CLICK, traceFunctionHolder, false); target.removeEventListener(MouseEvent.MIDDLE_CLICK, traceFunctionHolder, false); target.removeEventListener(MouseEvent.MIDDLE_MOUSE_DOWN, traceFunctionHolder, false); target.removeEventListener(MouseEvent.MIDDLE_MOUSE_UP, traceFunctionHolder, false); target.removeEventListener(MouseEvent.MOUSE_DOWN, traceFunctionHolder, false); target.removeEventListener(MouseEvent.MOUSE_MOVE, traceFunctionHolder, false); target.removeEventListener(MouseEvent.MOUSE_OUT, traceFunctionHolder, false); target.removeEventListener(MouseEvent.MOUSE_OVER, traceFunctionHolder, false); target.removeEventListener(MouseEvent.MOUSE_UP, traceFunctionHolder, false); target.removeEventListener(MouseEvent.MOUSE_WHEEL, traceFunctionHolder, false); target.removeEventListener(MouseEvent.RIGHT_CLICK, traceFunctionHolder, false); target.removeEventListener(MouseEvent.RIGHT_MOUSE_DOWN, traceFunctionHolder, false); target.removeEventListener(MouseEvent.RIGHT_MOUSE_UP, traceFunctionHolder, false); target.removeEventListener(MouseEvent.ROLL_OUT, traceFunctionHolder, false); target.removeEventListener(MouseEvent.ROLL_OVER, traceFunctionHolder, false); } target.removeEventListener(Event.ENTER_FRAME, enterFunctionHolder); } /** * Returns a String representation of the given array of events as MouseEventEntry tags * * Example: */ public static function getEventsAsMXMLString(events:Array):String { var output:String = ""; for each (var e:Object in events) output += '\n'; return output; } /** * Returns a visual representation of the given sequence of mouse events. */ public static function getEventsAsPath(events:Array, recordedDPI:Number = NaN):Group { // create a new group to hold the path and circles var g:Group = new Group(); g.mouseEnabled = false; var p:Path = new Path(); p.stroke = new SolidColorStroke(0xFFFF00, 3); p.data = ""; g.addElement(p); // add a point to the path for each event for (var i:int = 0; i < events.length; i++) { var e:Object = events[i]; // scale the co-ordinates if DPI scaling is requested var adjustedLocalX:Number = scaleByDPIRatio(e.localX, recordedDPI); var adjustedLocalY:Number = scaleByDPIRatio(e.localY, recordedDPI); if (i == 0) p.data += "M " + adjustedLocalX + " " + adjustedLocalY + " "; else p.data += "L " + adjustedLocalX + " " + adjustedLocalY + " "; if (e.type == MouseEvent.MOUSE_DOWN) { // draw a mouse down circle (green) var downCircle:Ellipse = createCircle(10, 0x00FF00); downCircle.x = adjustedLocalX - downCircle.width / 2; downCircle.y = adjustedLocalY - downCircle.height / 2; g.addElement(downCircle); } if (e.type == MouseEvent.MOUSE_UP) { // draw a mouse up circle (red) var upCircle:Ellipse = createCircle(10, 0xFF0000); upCircle.x = adjustedLocalX - upCircle.width / 2; upCircle.y = adjustedLocalY - upCircle.height / 2; g.addElement(upCircle); } } return g; } /** * Returns a circle of the given radius and fill color */ private static function createCircle(radius:Number, fillColor:uint):Ellipse { var c:Ellipse = new Ellipse(); c.width = radius; c.height = radius; var solidColor:SolidColor = new SolidColor(fillColor); c.fill = solidColor; return c; } /** * Writes the given content string to the disk at location fileName. * * Useful for writing sequences of mouse events to disk on the phone and * transferring that off of the device. * * Sample usage: * * writeFileToDisk('/sdcard/Flex/QA/List/mouseEvents.txt', * TouchScrollingUtil.getEventsAsMXMLString(TouchScrollingUtil.recordedMouseEvents)); */ public static function writeFileToDisk(fileName:String, content:String):void { var file:File = new File (fileName); var fileStream:FileStream = new FileStream(); fileStream.open(file, FileMode.WRITE); fileStream.writeUTF(content); fileStream.close(); } /** * Basically just calls trace(), but only does so if the verbose flag * is set to true. This helps us avoid inflating the log with hundreds * of extra trace statements while running in Mustella. */ private static function traceLog(... rest):void { // don't trace this message if debugging isn't enabled if (!enableVerboseTraceOuput) return; // format the output the same way trace() does with a space // in between each piece of the rest array var output:String = ""; var i:int = 0; const restLength:int = rest.length; for (i = 0; i < restLength; i++) { output += rest[i]; if (i < restLength - 1) output += " "; } trace(output); } } }