Hack 96 Add Key Shortcuts to Your Site

figs/moderate.gif figs/hack96.gif

To make your application easier and faster to use (and more like a desktop application), associate a keystroke or key combination with each button in your SWF.

Adding keyboard shortcuts to each of your buttons makes your site more accessible. This hack shows how to implement shortcuts with a minimum of code. Detecting keypresses is a little more complex than just looking for a key being pressed. We have to be able to detect keyboard combinations, such as Ctrl-Q, and detect both when the key is pressed and when it is released.

Detecting Keypresses

To detect the state of the keyboard, you first have to set up a listener object that listens to the Key class. The following code traces a message every time it detects a keypress:

function down( ) {

  trace("detected!");

}

var keyListener:Object = new Object( );

keyListener.onKeyDown = down;

Key.addListener(keyListener);

Note that the Key class does not differentiate between uppercase and lowercase letters when looking for keyboard inputs. For example, the Key class returns the same keycode regardless of whether you have the Shift key down or the Caps Lock key enabled when you press the A key.

The trouble with our code is that it runs our function for as long as a key is held down, which makes it unsuitable for detecting keyboard shortcuts, because pressing and holding a key down will generate multiple events. To fix this, we need to switch our event handling around so that we see only one "detected!" message for each key down-up cycle. The following code does this:

function down( ) {

  trace("detected!");

  delete this.onKeyDown;

  this.onKeyUp = up;

}

function up( ) {

  this.onKeyUp = undefined;

  this.onKeyDown = down;

}

var keyListener:Object = new Object( );

keyListener.onKeyDown = down;

Key.addListener(keyListener);

As soon as a keyDown event is detected, the code switches to looking for the corresponding keyUp event. The keyUp event causes the cycle to restart, creating a set of events that respond to the full keystroke toggle.

Note that this code works even if you attempt a keystroke combination, such as Ctrl-A. In that case, you will see a "detected!" message for each key.

Turning Keystrokes into Inputs

So far, we have only the ability to detect keystrokes. We really need to be able to detect which keys were pressed. We can do this with the Key.isDown( ) and Key.getCode( ) methods. The first tells you whether a particular key is pressed, and the second tells you the keycode of the last key that caused a key event (i.e., the last key that changed its state, which will not necessarily be the key that is currently held down). For example, if you hold down the A and S keys at the same time, then release the A key, the getCode( ) method returns the code for the A key, even though the S key is still down.

Note that the Key class returns keycode values for the physical keys themselves and does not take modifiers into account. For example, pressing the X key will give you the same code regardless of the state of the Shift or Ctrl keys.

The online help has a table listing the alphanumeric keycodes. Search on "Keyboard keys and keycode values" to find them. For nonalphanumeric keys, such as the arrow keys and spacebar, you can use the constant properties of the Key class. For example, Key.SPACE contains the keycode for the spacebar and Key.CONTROL contains the keycode for the Ctrl key.

For example:

x = Key.isDown(65); // x is true if the "A" key is pressed

y = Key.getCode( );  // y is 65 if the last key to cause an event was "A"

Knowing a key's keycode, we can quickly write ActionScript that detects single keystrokes. Assuming you have three buttons on the Stage with instance names buttonA, buttonB, and buttonC, the following code causes each button's onRelease( ) handler to execute if you click button A, B, or C. It also gives selection focus to the button associated with a keystroke, which (among other things) causes a yellow rectangle to be drawn around the button:

function aHandler( ) {

  trace("you clicked A");

}

function bHandler( ) {

  trace("you clicked B");

}

function cHandler( ) {

  trace("you clicked C");

}

function down( ) {

  if (keys[Key.getCode( )] != undefined) {

    keys[Key.getCode( )].onRelease( );

    Selection.setFocus(keys[Key.getCode( )]);

  }

  this.onKeyDown = undefined;

  this.onKeyUp = up;

}

function up( ) {

  this.onKeyUp = undefined;

  this.onKeyDown = down;

}

//

var keys:Array = new Array( );

var keyListener:Object = new Object( );

keys[65] = buttonA;

keys[66] = buttonB;

keys[67] = buttonC;

buttonA.onRelease = aHandler;

buttonB.onRelease = bHandler;

buttonC.onRelease = cHandler;

keyListener.onKeyDown = down;

Key.addListener(keyListener);

The preceding code creates an array called keys whose indexes are used to identify associated Flash buttons. For example, keys[65] contains a reference to buttonA, so when the A key (keycode 65) is pressed, the event handler buttonA.onRelease( ) is run. In Figure 11-22, the left and right images show the buttons buttonA, buttonB, and buttonC before (left) and after (right) the keyboard A key is pressed. When the A key is pressed, the message "You clicked A" appears in the Output panel.

Figure 11-22. Unselected buttons (left); buttonA selected (right)
figs/flhk_1122.gif


For the Flash Player to be able to capture keystrokes, the SWF must have browser focus [Hack #95] . The SWF maintains focus if the SWF within the browser has been clicked on, until other content outside the SWF is clicked on. Simply moving the mouse causes a button to lose browser focus, so you may never see the yellow bounding rectangle if you are holding the mouse when pressing A.

To test a SWF that needs to capture keystrokes, you should select ControlDisable Shortcuts once in Test Movie mode. This prevents the Flash application from capturing keyboard inputs for its own keyboard shortcuts and masking them from the SWF under test.

Turning Combos into Inputs

As well as having single keystroke shortcuts, many applications have multiple keystroke combinations (also known as combos), such as Ctrl-F1. Flash is sometimes used as a standalone desktop application, so it would be reasonable to use combos as Flash inputs.

However, you need to be careful about using combos as inputs into Flash, given that Flash is rarely at the top of the pecking order for receiving combos. Most of the common combos are already taken by the operating system or the browser, so Flash will not receive them all.

We can make our code look for combo keystrokes by specifying an array that defines a number of keys that need to be held down to form an input.

Suppose we wanted to make:

  • A the keystroke for buttonA

  • Ctrl-B the combo for buttonB

  • Ctrl-D the combo for buttonC

We can define this information in an array of objects:

keys[0] = {btn:buttonA, combo:[65]};

keys[1] = {btn:buttonB, combo:[Key.CONTROL, 66]};

keys[2] = {btn:buttonC, combo:[Key.CONTROL, 68]};

Each of our keys entries is now an object consisting of two properties:

  • The keys[].btn property tells us the Flash button with which to associate the combo.

  • The keys[].combo property is an array containing the individual keys that make up each combo.

The following code uses this data structure to add the keyboard combos for our three buttons, buttonA, buttonB, and buttonC. The hacky part of this code is the way we check whether the full combo is down. For each combo, the code:

  • Sets a Boolean, allKeys, to true prior to searching through each combo entry, keys[j].combo.

  • For each key in the combo, tests whether each key is down via Key.isDown(keys[j].combo[i]). This expression is true if the key is down and false if it is not. This value is ANDed (&&) with allKeys. If any key is not down, allKeys becomes false and remains so until the end of the loop.

  • At the end of the search, if allKeys is still true, we know that the full combo was depressed.

The cool thing about using the && logic operator in this way is that allKeys doesn't care in which order the keys were pressed or how many keys there are. We test whether they are all down whenever we carry out the test. If allKeys is true, the code carries on in much the same way as the preceding single keystroke code. The following code demonstrates (changes are shown in bold):

function aHandler( ) {

  trace("you clicked A");

}

function bHandler( ) {

  trace("you clicked B");

}

function cHandler( ) {

  trace("you clicked C");

}

function down( ) {

  var allKeys;

  //trace("--------------");

  for (var j = 0; j < keys.length; j++) {

    allKeys = true;

    for (var i = 0; i < keys[j].combo.length; i++) {

      allKeys = allKeys && Key.isDown(keys[j].combo[i]);

    }

    if (allKeys) {

      Selection.setFocus(keys[j]);

      //trace("combo detected");

      this.onKeyDown = undefined;

      this.onKeyUp = up;

      keys[j].btn.onRelease( );

      break;

    }

  }

}

function up( ) {

  //trace("up");

  this.onKeyUp = undefined;

  this.onKeyDown = down;

}

//

var keys:Array = new Array( );

var keyListener:Object = new Object( );

keys[0] = {btn:buttonA, combo:[65]};

keys[1] = {btn:buttonB, combo:[Key.CONTROL, 66]}; // Ctrl-B

keys[2] = {btn:buttonC, combo:[Key.CONTROL, 68]}; // Ctrl-D

buttonA.onRelease = aHandler;

buttonB.onRelease = bHandler;

buttonC.onRelease = cHandler;

keyListener.onKeyDown = down;

Key.addListener(keyListener);

I have left in some useful trace( ) actions, which I used in developing this code in the listing. If you want to see how (and in what order) the code detects and sets off the keyDown( ), keyUp( ), and onRelease( ) event handlers, you can uncomment the trace( ) statements and view the order of events in the Output panel. It should make the code much easier to understand.

The Ctrl-B shortcut won't work when you test the movie in Flash unless you disable keyboard shortcuts, because Ctrl-B opens the Bandwidth Profiler. Ctrl-C doesn't work because it is captured by other applications, most likely your operating system's copy feature so we use Ctrl-D as the keyboard combination for buttonC.

If you test the SWF in the browser, you will also see that Ctrl-B may cause problems (it is the shortcut for organizing your Favorites links in Internet Explorer). The moral of this is to check that your combos are not used by anything else that might be running at the same time as the Flash SWF; otherwise, Flash will not see them!

Final Thoughts

Accessibility is becoming more of a required feature as time goes on. Making your site respond to the keyboard as well as the mouse makes it more useful to the sight impaired but also makes it more accessible to the command-line twitch-typer. Surprisingly few Flash sites support keyboard shortcuts, probably because the code to do this is not totally obvious. Well, it is now, so there's no excuse!