//////////////////////////////////////////////////////////////////////////////// // // 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 spark.automation.delegates { import flash.display.DisplayObject; import flash.events.Event; import flash.events.FocusEvent; import flash.events.IEventDispatcher; import flash.events.KeyboardEvent; import flash.events.MouseEvent; import flash.events.TextEvent; import flash.ui.Keyboard; import flashx.textLayout.operations.CutOperation; import flashx.textLayout.operations.PasteOperation; import mx.automation.Automation; import mx.automation.IAutomationManager; import mx.automation.IAutomationObject; import mx.automation.IAutomationObjectHelper; import mx.automation.events.AutomationEvent; import mx.automation.events.TextSelectionEvent; import mx.core.EventPriority; import mx.core.mx_internal; import mx.events.SandboxMouseEvent; import mx.managers.IFocusManager; import mx.managers.IFocusManagerComponent; import mx.managers.IFocusManagerContainer; import mx.managers.ISystemManager; import mx.resources.IResourceManager; import mx.resources.ResourceManager; import spark.components.RichEditableText; import spark.events.TextOperationEvent; use namespace mx_internal; [ResourceBundle("automation")] /** * Utility class that facilitates replay of text input and selection. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 4 */ public class SparkRichEditableTextAutomationHelper { include "../../core/Version.as"; //-------------------------------------------------------------------------- // // Constructors // //-------------------------------------------------------------------------- /** * Constructor. * * @param owner The UIComponent that is using the TextField. For example, if a * TextArea is using the TextField, then the TextArea is the owner. * * @param replayer The IAutomationObject of the component. * * @param richEditableText The TextField object inside the component. * * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 4 */ public function SparkRichEditableTextAutomationHelper(owner:IEventDispatcher, replayer:IAutomationObject, richEditableText:spark.components.RichEditableText) { super(); // for spark, we will be called by the RicheEditableText only. // we could have moved this code to the delegate itself // to have the similar work flow for the halo, we kept the helper class // storing the passed variable. this.owner = owner; this.replayer = replayer; this.richEditableText = richEditableText; // adding the focus in handler so that we can add the appropriate // listeneres. this.owner.addEventListener(FocusEvent.FOCUS_IN, focusInHandler, false, EventPriority.DEFAULT-100, true); this.richEditableText.addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler, false, EventPriority.DEFAULT, true); // capture the slection captureSelection(); oldSelection = currentSelection; hasSelectionChanged = false; // capture the focus details if(recording) checkInitialFocus(); else Automation.automationManager.addEventListener(AutomationEvent.BEGIN_RECORD, beginRecordingHandler, false, 0 , true); } //-------------------------------------------------------------------------- // // Variables // //-------------------------------------------------------------------------- /** * @private */ private var stringBuffer:String; /** * @private */ private var owner:IEventDispatcher; /** * @private */ private var replayer:IAutomationObject; /** * @private */ private var richEditableText:spark.components.RichEditableText; /** * @private */ private var currentSelection:Array = null; /** * @private */ private var oldSelection:Array = null; /** * @private */ private var hasSelectionChanged:Boolean = false; /** * @private */ private var isWatchingFocus:Boolean = false; /** * @private */ private var isInInsertMode:Boolean = false; /** * @private * Used for accessing localized Error messages. */ private var resourceManager:IResourceManager = ResourceManager.getInstance(); //-------------------------------------------------------------------------- // // Properties // //-------------------------------------------------------------------------- //---------------------------------- // recording //---------------------------------- /** * @private */ private function get recording():Boolean { return Automation.automationManager && (Automation.automationManager as IAutomationManager).recording; } //-------------------------------------------------------------------------- // // Methods // //-------------------------------------------------------------------------- /** * @private */ private function flushCharacterBuffer():void { // this methods is called whenever the focus on the text is changed // so if the text value is changed, the change will be recorded here. // unlike other normal classes, we are not recording the changes as // and when the change happens. // reason is that we want one record for the multiple inputs, instead // of recording for each input. if (stringBuffer != null) { flushSelection(); var e:TextEvent = new TextEvent(TextEvent.TEXT_INPUT); if(stringBuffer) e.text = stringBuffer.toString(); else e.text=""; stringBuffer = null; recordAutomatableEvent(e); } } /** * @private */ private function captureSelection():void { // capture the selectio details and when the focus changes // if the selection details are changed, it will be recorded. currentSelection = [ richEditableText.selectionAnchorPosition, richEditableText.selectionActivePosition ]; hasSelectionChanged = oldSelection == null || oldSelection[0] != currentSelection[0] || oldSelection[1] != currentSelection[1]; } /** * @private */ private function flushSelection():void { // when the focus if (!hasSelectionChanged) return; // recording the details of the selection change. if (currentSelection && currentSelection[0] >= 0 && currentSelection[1] >= 0) { var e:TextSelectionEvent = new TextSelectionEvent(); e.beginIndex = currentSelection[0]; e.endIndex = currentSelection[1]; oldSelection = currentSelection; currentSelection = null; recordAutomatableEvent(e); } } /** * @private */ private function get hasSelection():Boolean { // checking whether a content is selected. return (richEditableText.selectionActivePosition != richEditableText.selectionAnchorPosition); } /** * @private */ protected function checkInitialFocus():void { //check whether we have already focus so that we can prepare for user input var o:DisplayObject = DisplayObject(richEditableText) ; while (o) { if (o is IFocusManagerContainer) break ; o = o.parent; } if (o) { var focusManager:IFocusManager = IFocusManagerContainer(o).focusManager; var focusObj:DisplayObject = focusManager ? DisplayObject(focusManager.getFocus()) : null; if (focusObj == owner) focusInHandler(null); } } /** * Records the user interaction with the text control. * * @param interaction The event to record. * * @param cacheable Contains true if this is a cacheable event, and false if not. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 4 */ public function recordAutomatableEvent(interaction:Event, cacheable:Boolean = false):void { var am:IAutomationManager = Automation.automationManager; am.recordAutomatableEvent(replayer, interaction, cacheable); } /** * Replays TextEvens, Selection Event, and type events. TypeEvents and Text events are replayed * depending on the character typed. Both dispatches the origin keystrokes. * This is necessary to mimic the original behavior, in case any components are * listening to keystroke events (for example, DataGrid listens to itemRenderer events, * or if a custom component is trying to do key masking). In Halo, the text events were changing * the contents using the text related methods as the flash player was ignoring the key evens. * In Gumbo this is not the case, so for the text and type events, we need only to send the key strokes. * dispatch the original keystrokes, but the Flash Player richEditableText ignores * the events we are sending it. * * @param event Event to replay. * * @return If true, replay the event. * * @langversion 3.0 * @playerversion Flash 9 * @playerversion AIR 1.1 * @productversion Flex 4 */ public function replayAutomatableEvent(event:Event):Boolean { var changeEvent:TextOperationEvent = new TextOperationEvent(TextOperationEvent.CHANGE); var sm:ISystemManager = Automation.getMainApplication().systemManager; var help:IAutomationObjectHelper = Automation.automationObjectHelper; var ke:KeyboardEvent; if (event is MouseEvent && event.type == MouseEvent.CLICK) return help.replayClick(owner, event as MouseEvent); else if (event is TextSelectionEvent) { // we should reply click, as click is not recorded during selection. // so if anybody listening to the click event, they should get it. help.replayClick(owner); if(owner as IFocusManagerComponent) IFocusManagerComponent(owner).setFocus(); else richEditableText.setFocus(); // selection replay is done by calling the underlying select method. // recorded value is anchorPosition and active position in order. // Since we have used the existing TextSelection var selectionEvent:TextSelectionEvent = TextSelectionEvent(event); richEditableText.selectRange(selectionEvent.beginIndex, selectionEvent.endIndex); return true; } else if (event is TextEvent) { // need to set focus in order for the uirichEditableText to behave correctly if(owner as IFocusManagerComponent) IFocusManagerComponent(owner).setFocus(); else richEditableText.setFocus(); var textEvent:TextEvent = TextEvent(event); var text:String = textEvent.text; var n:int = textEvent.text.length; if(n == 0) { // if there is any selection we want // to clear it, as this recording would have // been resulted from cut operation. richEditableText.insertText(""); } // it was seen that at times event dispathing does not change // the string and at times it changes. // refer http://bugs.adobe.com/jira/browse/FLEXENT-1179 // We need to insert the text if the change has not happened. // for this we need to find the string before and after the event dispatch. // but the text does not display the correct value after the event dispatch. It shows // only after we do an insert operation on the text after the same. // but to do the inser operation, we need to find the active and anchor position also. // this also does not change when the value is dispatched and still is at the beginning of the string. // so we need to calcualte it. Here we need to consider the direction. var stringBeforeChange:String = richEditableText.text; var insertPos:int = richEditableText.selectionActivePosition; var activePos:int = insertPos; var anchorPos:int = richEditableText.selectionAnchorPosition; var direction:int = -1; if(richEditableText && richEditableText.textFlow && richEditableText.textFlow.computedFormat && richEditableText.textFlow.computedFormat.direction) { if (richEditableText.textFlow.computedFormat.direction=="ltr") direction = 1; } richEditableText.selectRange(anchorPos, activePos); for (var i:uint = 0; i < n; i++) { ke = new KeyboardEvent(KeyboardEvent.KEY_DOWN); ke.charCode = text.charCodeAt(i); ke.keyCode = text.charCodeAt(i); richEditableText.dispatchEvent(ke); // we dont need any special handling on the string. // replaying the key board events takes care of the needed. // however when there is a line feed or carriage return , we need to handle it specially if((text.charAt(i) == "\n") || (text.charAt(i) == "\r")) richEditableText.insertText(text.charAt(i)); var te:TextEvent = new TextEvent(TextEvent.TEXT_INPUT); te.text = String(text.charAt(i)); // ref. http://bugs.adobe.com/jira/browse/FLEXENT-838 - charcode vs charAt richEditableText.dispatchEvent(te); // we dont need to select as the previous selection would have taken care of the // requried selection ke = new KeyboardEvent(KeyboardEvent.KEY_UP); ke.charCode = text.charCodeAt(i); ke.keyCode = text.charCodeAt(i); richEditableText.dispatchEvent(ke); // dispatch a change event to indicate that the value is changed. richEditableText.dispatchEvent(changeEvent); // calculate the new insert position. insertPos += direction*1; } if(text.length > 0) { // do the operation to reflect the text value richEditableText.selectRange(insertPos,insertPos); richEditableText.insertText(""); // check whethr the string is changed after the event dispatch // refer http://bugs.adobe.com/jira/browse/FLEXENT-1179 var stringAfterChange:String = richEditableText.text; if(stringBeforeChange == stringAfterChange) { richEditableText.insertText(text); } } return true; } else if (event is KeyboardEvent) { var kbEvent:KeyboardEvent = KeyboardEvent(event); var keyCode:int = kbEvent.keyCode; switch (keyCode) { case Keyboard.HOME: { break; } case Keyboard.END: { break; } case Keyboard.ENTER: { // replaying the keyboard events will do the needful // we dont need to handle this. // replace the selected text with newline //if (richEditableText.multiline) // richEditableText.insertText("\n"); break; } case Keyboard.BACKSPACE: { // we dont need a manual handling here. // replaying the key up and down will do the needfule break; } case Keyboard.DELETE: { // we dont need a manual handling here. // replaying the key up and down will do the needfule break; } case Keyboard.INSERT: { isInInsertMode = !isInInsertMode; break; } case Keyboard.ESCAPE: { break; } default: { var message:String = resourceManager.getString( "automation", "notReplayable", [keyCode]); throw new Error(message); } } ke = new KeyboardEvent(KeyboardEvent.KEY_DOWN); ke.charCode = keyCode; ke.keyCode = keyCode; ke.ctrlKey = kbEvent.ctrlKey; ke.shiftKey = kbEvent.shiftKey; ke.altKey = kbEvent.altKey; richEditableText.dispatchEvent(ke); ke = new KeyboardEvent(KeyboardEvent.KEY_UP); ke.charCode = keyCode; ke.keyCode = keyCode; ke.ctrlKey = kbEvent.ctrlKey; ke.shiftKey = kbEvent.shiftKey; ke.altKey = kbEvent.altKey; richEditableText.dispatchEvent(ke); richEditableText.dispatchEvent(changeEvent); return true; } return false; } //-------------------------------------------------------------------------- // // Event handlers // //-------------------------------------------------------------------------- /** * @private */ private function focusInHandler(event:FocusEvent):void { if (!recording) return; if (!isWatchingFocus) { isWatchingFocus = true; //Add the focus change listeners as low priority //so that any code that may prevent default (prevent //the focus change) gets a chance to execute before //getting to us. We only want to process the event //if the focus really is going to change. richEditableText.addEventListener(FocusEvent.KEY_FOCUS_CHANGE, focusOutHandler, false, EventPriority.DEFAULT-1000, true); //Use FOCUS_OUT instead of MOUSE_FOCUS_CHANGE never //really gets fired because the player doesn't initiate //mouse focus changes (except when a text field gets //focus). Our mouseDownOutside handler should take //care of flushing events before a new item gets focus //and we may not even need this event handler richEditableText.addEventListener(FocusEvent.FOCUS_OUT, focusOutHandler, false, EventPriority.DEFAULT, true); //In case someone clicks elsewhere but we don't loose the focus //we need to flush, i.e. they click a button that generates a click //we need to beat them and record our events first //var sm:ISystemManager = Application.application.systemManager; var sm:ISystemManager = Automation.getMainApplication().systemManager; sm.getSandboxRoot().addEventListener(MouseEvent.MOUSE_DOWN, mouseDownOutsideHandler, true, EventPriority.DEFAULT, true); sm.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_DOWN_SOMEWHERE, mouseDownOutsideHandler, true, EventPriority.DEFAULT, true); sm.addEventListener(Event.DEACTIVATE, stageEventHandler, false, EventPriority.DEFAULT+1, true); sm.getSandboxRoot().addEventListener(Event.MOUSE_LEAVE, stageEventHandler, false, EventPriority.DEFAULT+1, true); sm.getSandboxRoot().addEventListener(MouseEvent.MOUSE_DOWN, stageEventHandler, true, EventPriority.DEFAULT+1, true); sm.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_DOWN_SOMEWHERE, stageEventHandler, true, EventPriority.DEFAULT+1, true); richEditableText.addEventListener(TextEvent.TEXT_INPUT, textInputHandler, false, EventPriority.DEFAULT+100, true); richEditableText.addEventListener(TextOperationEvent.CHANGING, changingTimeDataCapturer, false, EventPriority.DEFAULT+50, true); richEditableText.addEventListener(TextOperationEvent.CHANGE, changeHandler, false, EventPriority.DEFAULT+50, true); richEditableText.addEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler, false, EventPriority.DEFAULT, true); richEditableText.addEventListener(KeyboardEvent.KEY_UP, keyUpHandler, false, EventPriority.DEFAULT, true); //need to cache selection so it is not recorded unless it changes captureSelection(); oldSelection = currentSelection; hasSelectionChanged = false; } } /** * @private */ private function stageEventHandler(event:Event):void { //Don't call focusOutHandler, that would remove our event listeners //which would be bad because a deactive and mouse leave doesn't mean //the framework thinks we lost focus, framework should call focus out //if it does intend to remove focus during a deactive flushSelection(); flushCharacterBuffer(); } /** * @private */ private function mouseDownOutsideHandler(event:Event):void { if (event.target != richEditableText) { //Don't call focusOutHandler, that would remove our event listeners //which would be bad because it's possible for someone to click outside //of the richEditableText but not have the focus change. Just flush the //event buffers in case that mouse down outside causes an event to be recorded flushSelection(); flushCharacterBuffer(); } } /** * @private */ private function focusOutHandler(event:Event):void { if (isWatchingFocus && !event.isDefaultPrevented()) { isWatchingFocus = false; if (richEditableText) { richEditableText.removeEventListener(FocusEvent.KEY_FOCUS_CHANGE, focusOutHandler, false); richEditableText.removeEventListener(FocusEvent.FOCUS_OUT, focusOutHandler, false); } var sm:ISystemManager = Automation.getMainApplication().systemManager; sm.removeEventListener(MouseEvent.MOUSE_DOWN, mouseDownOutsideHandler, true); sm.removeEventListener(Event.DEACTIVATE, stageEventHandler, false); sm.getSandboxRoot().removeEventListener(Event.MOUSE_LEAVE, stageEventHandler, false); sm.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_DOWN, stageEventHandler, true); sm.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_DOWN_SOMEWHERE, stageEventHandler, true); richEditableText.removeEventListener(TextOperationEvent.CHANGE,changeHandler); richEditableText.removeEventListener(TextOperationEvent.CHANGING,changingTimeDataCapturer ); richEditableText.removeEventListener(TextEvent.TEXT_INPUT, textInputHandler); richEditableText.removeEventListener(KeyboardEvent.KEY_DOWN, keyDownHandler); richEditableText.removeEventListener(KeyboardEvent.KEY_UP, keyUpHandler); flushSelection(); flushCharacterBuffer(); } } /** * @private */ private function mouseDownHandler(event:MouseEvent):void { if (!recording) return; richEditableText.systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_UP, mouseUpHandler, false, EventPriority.DEFAULT, true); richEditableText.systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpHandler, false, EventPriority.DEFAULT, true); richEditableText.addEventListener(MouseEvent.DOUBLE_CLICK, mouseDoubleClickHandler, false, EventPriority.DEFAULT-1, true); } /** * @private */ private function mouseClickHandler(event:MouseEvent):void { if (!recording) return; richEditableText.removeEventListener(MouseEvent.CLICK, mouseClickHandler); recordAutomatableEvent(event); } private function mouseDoubleClickHandler(event:MouseEvent):void { if (!recording) return; captureSelection(); } /** * @private */ private function mouseUpHandler(event:Event):void { if (!recording) return; richEditableText.systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_UP, mouseUpHandler); richEditableText.systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, mouseUpHandler); flushCharacterBuffer(); captureSelection(); hasSelectionChanged = true; } /** * @private */ private function keyDownHandler(event:KeyboardEvent):void { if (!recording) return; //arrow and navigation keys should dispatch whatever was last typed //backspace, delete, and enter are dispatched switch (event.keyCode) { case Keyboard.CONTROL: { flushCharacterBuffer(); break; } case Keyboard.SHIFT: { break; } case Keyboard.DOWN: case Keyboard.END: case Keyboard.HOME: case Keyboard.LEFT: case Keyboard.PAGE_DOWN: case Keyboard.PAGE_UP: case Keyboard.RIGHT: case Keyboard.UP: { flushCharacterBuffer(); break; } case Keyboard.INSERT: case Keyboard.BACKSPACE: case Keyboard.DELETE: case Keyboard.ENTER: { flushSelection(); flushCharacterBuffer(); recordAutomatableEvent(event); oldSelection = null; break; } case Keyboard.ESCAPE: { recordAutomatableEvent(event); break; } default: { break; } } } /** * @private */ private function keyUpHandler(event:KeyboardEvent):void { if (!recording) return; //arrow and navigation keys should dispatch whatever was last typed //backspace, delete, and enter are dispatched switch (event.keyCode) { case Keyboard.TAB: { break; } case Keyboard.SHIFT: { break; } case Keyboard.DOWN: case Keyboard.END: case Keyboard.HOME: case Keyboard.LEFT: case Keyboard.PAGE_DOWN: case Keyboard.PAGE_UP: case Keyboard.RIGHT: case Keyboard.UP: { captureSelection(); break; } case Keyboard.BACKSPACE: case Keyboard.DELETE: case Keyboard.ENTER: { break; } case Keyboard.CONTROL: { captureSelection(); break; } default: { if (event.ctrlKey) { flushSelection(); flushCharacterBuffer(); } break; } } } /** * @private */ private function textInputHandler(event:TextEvent):void { if (!recording) return; // The \n will be caught by the ENTER capture if ((event.text == "\n")||(event.text == "\r")) return; if (!stringBuffer) { flushSelection(); stringBuffer = ""; } // TextField allows a script to enter more text to be inserted than maxChars. // Hence we have to prevent the recording of more characters than maxChars. // Without this check playback will add more characters than maxChars leading to errors. if (richEditableText.maxChars == 0 || richEditableText.text.length < richEditableText.maxChars) { stringBuffer += event.text; oldSelection = null; } } private var currentActivePos:int = -1; private var currentLength:int = -1; private function changingTimeDataCapturer(event:TextOperationEvent):void { // we need to capture the details before the change // as we dont have a straight forward way of getting the change currentActivePos = richEditableText.selectionActivePosition; currentLength = richEditableText.text.length; } /** * @private */ private function changeHandler(event:TextOperationEvent):void { if (!recording) return; var operation:Object = event.operation; if(operation is PasteOperation) { var newLength:int = richEditableText.text.length; // get the additional string // TBD once we have a correct understanding about the rtl, we need to decide // what text to obtain for the rtl case. var additionalString:String = richEditableText.text.substr(currentActivePos,newLength-currentLength); if (!stringBuffer) { stringBuffer = ""; } stringBuffer += additionalString; // we need the charbuffer to be flushed // as the selection details can change after the same flushCharacterBuffer(); } else if(operation is CutOperation) { // we need to record an empty string here so that the current selection will be removed stringBuffer = ""; flushCharacterBuffer(); } } /** * @private */ private function beginRecordingHandler(event:Event):void { checkInitialFocus(); } } }