blob: b2b1d8315619b8698bc079696d41acde03d58500 [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// 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.events.Event;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.TextEvent;
import flash.system.Capabilities;
import flash.text.TextField;
import flash.ui.Keyboard;
/**
* The test step that fakes a keyboard event
* MXML attributes:
* type (optional)
* charCode
* ctrlKey (optional)
* keyCode (optional)
* keyLocation (optional)
* shiftKey (optional)
* waitTarget (optional)
* waitEvent (optional)
* timeout (optional);
* cancelable (optional)
*/
public class DispatchKeyEvent extends TestStep
{
// These are constants from flash.ui.Keyboard. They are not
// available in non-AIR compilations, so they are reproduced here
// to avoid compile errors.
// If they change in AIR, these will need to be updated.
public static const FLASH_UI_KEYBOARD_BACK:uint = 0x01000016;
public static const FLASH_UI_KEYBOARD_MENU:uint = 0x01000012;
public static const FLASH_UI_KEYBOARD_SEARCH:uint = 0x0100001F;
private var inDispatchKey:Boolean;
private var gotFocusIn:Boolean;
private var charSequence:Array;
private var keySequence:Array;
private var currentRepeat:int;
private var currentKey:int;
private var sendBoth:Boolean;
/**
* Set the target's property to the specified value
*/
override protected function doStep():void
{
UnitTester.blockFocusEvents = false;
sendBoth = false;
if (!type)
{
sendBoth = true;
type = "keyDown";
}
var i:int;
var n:int;
charSequence = new Array();
keySequence = new Array();
if (charCode)
{
charSequence.push(charCode);
keySequence.push(keyCode ? keyCode : CharCodeToKeyCode[charCode] ? CharCodeToKeyCode[charCode] : charCode);
}
else if (keyCode)
{
charSequence.push(KeyCodeToCharCode[keyCode] ? KeyCodeToCharCode[keyCode] : 0);
keySequence.push(keyCode);
}
else if (char)
{
n = char.length;
for (i = 0; i < n; i++)
{
var c:uint = char.charCodeAt(i)
charSequence.push(c);
keySequence.push(CharCodeToKeyCode[c]);
}
}
else if (key || keys)
{
var sequence:Array;
if (key)
sequence = [ key ];
else
sequence = keys;
n = sequence.length;
for (i = 0; i < n; i++)
{
var kc:uint = Keyboard[sequence[i]];
if (kc == 0)
{
testResult.doFail(key + " is not a valid flash.ui.Keyboard constant");
UnitTester.blockFocusEvents = true;
return;
}
keySequence.push(kc);
charSequence.push(KeyCodeToCharCode[kc] ? KeyCodeToCharCode[kc] : 0);
}
}
else
{
testResult.doFail("no keys specified");
UnitTester.blockFocusEvents = true;
return;
}
try
{
for (i = 0; i < repeatCount; i++)
{
var m:int = charSequence.length;
for (var j:int = 0; j < m; j++)
{
var event:KeyboardEvent = new KeyboardEvent(type, true, cancelable); // all keyboard events bubble
event.ctrlKey = ctrlKey;
event.shiftKey = shiftKey;
event.charCode = charSequence[j];
event.keyCode = keySequence[j];
event.keyLocation = keyLocation;
if (keySequence[j] == Keyboard.TAB)
{
// if we don't see a focusIn, focus is being set
// asynchronously so we need to wait.
currentRepeat = i;
currentKey = j;
gotFocusIn = false;
root.addEventListener("focusIn", focusInHandler);
inDispatchKey = true;
dispatchKey(j, event);
inDispatchKey = false;
if (!gotFocusIn)
break;
}
else
dispatchKey(j, event);
}
}
}
catch (e1:Error)
{
TestOutput.logResult("Exception thrown in DispatchKeyEvent.");
testResult.doFail (e1.getStackTrace());
}
UnitTester.blockFocusEvents = true;
}
/**
* (Optional) name of a UI object whose Window/Stage
* will be used to dispatch the event
*/
public var window:String;
/**
* The type of the event to send (keyUp, keyDown, etc).
* If not set, we'll send both a keyDown and a keyUp
*/
public var type:String;
/**
* The char or sequence of chars to send as a string/char if you don't know the charCode (optional)
*/
public var char:String;
/**
* The charCode property on the KeyboardEvent (optional)
*/
public var charCode:uint;
/**
* The ctrlKey property on the KeyboardEvent (optional)
*/
public var ctrlKey:Boolean;
/**
* The Keyboard key if you don't know the keyCode (optional)
*/
public var key:String;
/**
* The sequence of keys (optional) e.g ["LEFT", "UP"]
*/
public var keys:Array;
/**
* The keyCode property on the KeyboardEvent (optional)
*/
public var keyCode:uint;
/**
* The keyLocation property on the KeyboardEvent (optional)
*/
public var keyLocation:uint;
/**
* The number of times to repeat the sequence (optional)
*/
public var repeatCount:uint = 1;
/**
* The shiftKey property on the KeyboardEvent (optional)
*/
public var shiftKey:Boolean;
/**
* Designate the created event to be cancelable. by default, they are not
*/
public var cancelable:Boolean = false;
/**
* The FlashPlayer TextField doesn't actually handle keyboard events so we have to
* emulate them
*/
private function emulateKey(actualTarget:Object, event:KeyboardEvent):void
{
var begin:int = actualTarget.selectionBeginIndex;
var end:int = actualTarget.selectionEndIndex;
var caret:int = actualTarget.caretIndex;
// trace("begin =", begin, "end =", end, "caret =", caret);
if (event.keyCode == Keyboard.LEFT)
{
if (event.shiftKey)
{
if (caret > 0)
{
if (caret == begin)
{
begin--;
// last param defines caret position
actualTarget.setSelection(end, begin);
}
else if (caret == end)
{
end--;
if (end < begin)
begin = end;
actualTarget.setSelection(end, begin);
}
}
}
else
{
if (begin != end)
actualTarget.setSelection(begin, begin);
else if (caret > 0)
actualTarget.setSelection(caret - 1, caret - 1);
}
}
else if (event.keyCode == Keyboard.RIGHT)
{
if (event.shiftKey)
{
if (caret < actualTarget.length)
{
if (caret == end)
{
end++;
actualTarget.setSelection(begin, end);
}
else if (caret == begin)
{
begin++;
if (end < begin)
end = begin;
// last param defines caret position
actualTarget.setSelection(end, begin);
}
}
}
else
{
if (begin != end)
actualTarget.setSelection(end, end);
else if (caret > 0)
actualTarget.setSelection(caret + 1, caret + 1);
}
}
}
private function focusInHandler(focusEvent:Event):void
{
gotFocusIn = true;
root.removeEventListener("focusIn", focusInHandler);
if (inDispatchKey)
return;
for (var i:int = currentRepeat; i < repeatCount; i++)
{
var m:int = charSequence.length;
for (var j:int = currentKey + 1; j < m; j++)
{
var event:KeyboardEvent = new KeyboardEvent(type, true, cancelable); // all keyboard events bubble
event.ctrlKey = ctrlKey;
event.shiftKey = shiftKey;
event.charCode = charSequence[j];
event.keyCode = keySequence[j];
event.keyLocation = keyLocation;
if (keySequence[j] == Keyboard.TAB)
{
currentRepeat = i;
currentKey = j;
gotFocusIn = false;
root.addEventListener("focusIn", focusInHandler);
inDispatchKey = true;
dispatchKey(j, event);
inDispatchKey = false;
if (!gotFocusIn)
break;
}
else
dispatchKey(j, event);
}
}
}
private function dispatchKey(index:int, event:KeyboardEvent):void
{
// note that we don't check Window activation since we want to run in the background
// and window activation is a player function
var actualTarget:Object;
if (window)
{
actualTarget = context.stringToObject(window);
actualTarget = actualTarget.stage.focus;
}
else
{
actualTarget = root.stage.focus;
if (!actualTarget)
{
actualTarget = UnitTester.getFocus();
}
}
// BACK, MENU, and SEARCH are buttons on mobile (Android) devices.
// On Android devices right now, actualTarget is still null at this point. Dispatching the event to the stage works.
// Using the constants in flash.ui.Keyboard will cause an error in a non-AIR runs, so the constants are also defined
// in this file, above. There is risk here.
if (keySequence[index] == FLASH_UI_KEYBOARD_BACK ||
keySequence[index] == FLASH_UI_KEYBOARD_MENU ||
keySequence[index] == FLASH_UI_KEYBOARD_SEARCH){
actualTarget = root.stage;
}
if (actualTarget)
{
var targetType:TypeInfo = context.getTypeInfo(actualTarget);
var isTextView:Boolean = targetType.isAssignableTo("spark.components::RichEditableText");
if (actualTarget is TextField)
{
if (event.charCode)
{
if (actualTarget.type == "input")
{
actualTarget.replaceSelectedText(String.fromCharCode(event.charCode));
// actualTarget.dispatchEvent(new Event("change", true));
actualTarget.dispatchEvent(new Event("change"));
}
}
else
{
if (actualTarget.selectable)
emulateKey(actualTarget, event);
}
}
actualTarget.dispatchEvent(event);
if (isTextView)
{
if (event.keyCode == Keyboard.DELETE ||
event.keyCode == Keyboard.BACKSPACE ||
event.keyCode == Keyboard.INSERT ||
ctrlKey)
{
// don't send TEXT_INPUT event
}
else
{
var textEvent:TextEvent = new TextEvent(TextEvent.TEXT_INPUT, true, true);
textEvent.text = String.fromCharCode(charSequence[index]);
actualTarget.dispatchEvent(textEvent);
}
}
if (keySequence[index] == Keyboard.TAB && type == "keyDown")
{
var fm:Object;
var newTarget:Object = actualTarget;
while (!fm && newTarget)
{
if ("focusManager" in newTarget)
fm = newTarget["focusManager"];
newTarget = newTarget.parent;
}
newTarget = null;
if (fm)
{
try
{
newTarget = fm.getNextFocusManagerComponent(shiftKey);
}
catch (e:Error)
{
// ignore error thrown here. Should only throw if the
// current FM became inactive as a result of dispatching
// the key event. We don't really care too much about
// getting an accurate newTarget in this case because
// newTarget is often wrong since the Player is offering
// it up and the Player has that wonky algorithm for
// determining newTarget. In theory, none of our code
// truly cares as long as it doesn't point to old focus
// object.
}
}
actualTarget.dispatchEvent(new FocusEvent(FocusEvent.KEY_FOCUS_CHANGE, true, true, InteractiveObject(newTarget), shiftKey, Keyboard.TAB));
}
if (sendBoth)
{
event = new KeyboardEvent("keyUp", true, cancelable);
event.ctrlKey = ctrlKey;
event.shiftKey = shiftKey;
event.charCode = charSequence[index];
event.keyCode = keySequence[index];
event.keyLocation = keyLocation;
actualTarget.dispatchEvent(event);
}
}
else
{
if (keySequence[index] == Keyboard.TAB && type == "keyDown")
{
var thisRoot:DisplayObject
// note that we don't check Window activation since we want to run in the background
// and window activation is a player function
if (window)
{
thisRoot = context.stringToObject(window).root;
}
else
thisRoot = root;
try
{
thisRoot.stage.dispatchEvent(new FocusEvent(FocusEvent.KEY_FOCUS_CHANGE, true, true, InteractiveObject(actualTarget), shiftKey, Keyboard.TAB));
}
catch(se2:SecurityError)
{
thisRoot.dispatchEvent(new FocusEvent(FocusEvent.KEY_FOCUS_CHANGE, true, true, InteractiveObject(actualTarget), shiftKey, Keyboard.TAB));
}
}
}
}
private var KeyCodeToCharCode:Object = {
8: 8,
13: 13,
96: 48,
97: 49,
98: 50,
99: 51,
100: 52,
101: 53,
102: 54,
103: 55,
104: 56,
105: 57,
106: 42,
107: 43,
109: 45,
110: 46,
111: 47
}
private var CharCodeToKeyCode:Object = {
13: 13,
33: 49,
34: 222,
35: 51,
36: 52,
37: 53,
38: 55,
39: 222,
40: 57,
41: 48,
42: 56,
43: 187,
44: 188,
45: 189,
46: 190,
47: 191,
48: 48,
49: 49,
50: 50,
51: 51,
52: 52,
53: 53,
54: 54,
55: 55,
56: 56,
57: 57,
58: 186,
59: 186,
60: 188,
61: 187,
62: 190,
63: 191,
64: 50,
65: 65,
66: 66,
67: 67,
68: 68,
69: 69,
70: 70,
71: 71,
72: 72,
73: 73,
74: 74,
75: 75,
76: 76,
77: 77,
78: 78,
79: 79,
80: 80,
81: 81,
82: 82,
83: 83,
84: 84,
85: 85,
86: 86,
87: 87,
88: 88,
89: 89,
90: 90,
91: 219,
92: 220,
93: 221,
94: 54,
95: 189,
96: 192,
97: 65,
98: 66,
99: 67,
100: 68,
101: 69,
102: 70,
103: 71,
104: 72,
105: 73,
106: 74,
107: 75,
108: 76,
109: 77,
110: 78,
111: 79,
112: 80,
113: 81,
114: 82,
115: 83,
116: 84,
117: 85,
118: 86,
119: 87,
120: 88,
121: 89,
122: 90,
123: 219,
124: 220,
125: 221,
126: 192
}
/**
* customize string representation
*/
override public function toString():String
{
var s:String = "DispatchKeyEvent";
if (charCode)
s += ": charCode = " + charCode.toString();
if (keyCode)
s += ": keyCode = " + keyCode.toString();
if (char)
s += ": char = " + char;
if (key)
s += ": key = " + key;
if (keys)
s += ": keys = " + keys.toString();
if (type)
s += ", type = " + type;
if (shiftKey)
s += ", shiftKey = " + shiftKey.toString();
if (ctrlKey)
s += ", ctrlKey = " + ctrlKey.toString();
if (repeatCount)
s += ", repeatCount = " + repeatCount.toString();
return s;
}
}
}