O'Reilly    
 Published on O'Reilly (http://oreilly.com/)
 See this if you're having trouble printing code examples


Internationalization, Accessibility, and Printing: Chapter 24 - Flex 4 Cookbook

by Joshua Noble, Todd Anderson, Marco Casario, Garth Braithwaite
Flex 4 Cookbook

This excerpt is from Flex 4 Cookbook. This highly practical book contains hundreds of tested recipes for developing interactive Rich Internet Applications. You'll find everything from Flex basics to solutions for working with visual components and data access, as well as tips on application development, unit testing, and Adobe AIR. Each recipe features a discussion of how and why it works, and many of them offer sample code that you can put to use immediately.

buy button

Chapter 24. Internationalization, Accessibility, and Printing

To ensure that your applications are usable by the widest range of users, Flex 4 provides many accessibility, internationalization, and printing options. For example, if your project must comply with accessibility standards, you’ll find screen-reader detection and keyboard tab orders to help visually impaired users, or users for whom the use of a pointing device is difficult or impossible. Flex’s toolset for internationalization and localization was much improved in Version 4. The localization features include a built-in internationalization resource manager, runtime locale determination, runtime locale switching, and resource modules that can be requested at runtime. If your challenge is closer to home—say, if you need printed deliverables—the latest version of Flex has that covered too. Flex 4 enables you to print Flex components and includes a DataGrid component specifically for printing repetitive, multipage output.

This chapter contains recipes that will walk you through formatting many kinds of output for printing, including a solution for formatting collections of components for printing across multiple pages by using PrintDataGrid with a custom item renderer. Recipes for displaying non-Western characters, detecting screen readers, and defining tab orders help make your applications more accessible to visually impaired users. Finally, several techniques for localizing applications are presented.

Add an International Character Set to an Application

Problem

You need to display text from an ideogram-based language, such as Chinese or Korean, in your application.

Solution

Use embedded fonts to ensure that the appropriate font is available to the Flash Player.

Discussion

Flex applications are capable of rendering text using non-Western characters, including Unicode-encoded text such as Chinese or Korean characters, provided that a font including those characters is available to the Flash Player. Developers can ensure that the appropriate font is available by embedding it into the application the same way as you would any Western font. Be aware, however, that the ease of this method comes at a price: the large number of characters in most ideogram-based languages means a bulkier SWF file. When considering this approach, you must weigh the trade-off between the increased size of your SWF file and the proper rendering of your text. The following example, ChineseFonts.mxml, illustrates both embedding the font and loading the system font.ch23_FlashBuildDebugger.

Note

When you open ChineseFonts.mxml, you will see the Chinese text next to System Font only if your system’s font contains the required characters. The Embedded Font line will be displayed on all systems.

You can embed a font as shown here:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/halo" >


    <s:layout><s:BasicLayout/></s:layout>
    <fx:Style>
        @font-face
        {
            src:url("../assets/LiSongPro.ttf");
            fontFamily:"LiSong Pro";
            fontStyle:Light;
        }
    </fx:Style>
    <s:Panel title="Fonts" width="75%" height="75%"
             horizontalCenter="0" verticalCenter="0">
        <s:layout><s:VerticalLayout/></s:layout>
        <s:Label text="System Font"/>
        <s:Label text="快的棕色狐狸慢慢地跳過了懶惰灰色灰鼠" />

        <s:Label text="Embedded Font"/>
        <s:Label fontFamily="LiSong Pro"
        text="快的棕色狐狸慢慢地跳過了懶惰灰色灰鼠" />
    </s:Panel>

</s:Application>

Although the MXML source for the Unicode-encoded method is unremarkable, the XML data it loads contains text in simplified Chinese:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/halo"
               creationComplete="createComplete()">
    <s:layout><s:BasicLayout/></s:layout>

    <fx:Declarations>
        <fx:XML source="books.xml" id="booksData"/>
    </fx:Declarations>

    <fx:Style>
        @font-face
        {
            src:url("../assets/LiSongPro.ttf");
            fontFamily:"LiSong Pro";
            fontStyle:Light;
        }

    </fx:Style>

    <fx:Script>
        <![CDATA[
            import spark.components.Label;

            private function createComplete():void {

                var booklist:XMLList = booksData.book;
                for(var i:int = 0; i<booklist.length(); i++) {
                    var label:Label = new Label();                                 
                    label.text = booklist[i];
                    vgroup.addElement(label);
                }

            }

        ]]>
    </fx:Script>

    <s:VGroup id="vgroup"/>
</s:Application>

The following is the document loaded by the application:

<books>
   <book title="阿波罗为Adobe导电线开发商口袋指南">
      现在您能建立和部署基于闪光的富有的互联网应用(RIAs) 对桌面使用Adobe的导电线框架。
      由阿波罗产品队的成员写, 这是正式指南对于Adobe 阿波罗, 新发怒平台桌面运行时间阿
      尔法发行从Adobe实验室。众多的例子说明怎么阿波罗工作因此您可能立即开始大厦RIAs 
      为桌面。
   </book>
   <book title="编程的导电线2">
      编程的导电线2 谈论导电线框架在上下文。作者介绍特点以告诉读者不仅怎样, 而且原因
      为什么使用一个特殊特点, 何时使用它, 和何时不是的实用和有用的例子。这本书被写为
      发展专家。当书不假设观众早先工作了以一刹那技术, 读者最将受益于书如果他们早先建
      立了基于互联网,n tiered 应用。
   </book>
   <book title="ActionScript 3.0设计样式">
      如果您是老练的闪光或屈曲开发商准备好应付老练编程技术与ActionScript 3.0,
       这实践介绍逐步设计样式作为您通过过程。您得知各种各样的类型设计样式和修建小
       抽象例子在尝试您的手之前在大厦完全的运作的应用被概述在书。
   </book>
</books>

Use a Resource Bundle to Localize an Application

Problem

You need to support a small number of alternate languages in your application.

Solution

Use compiled-in resource bundles to provide localized assets.

Discussion

For basic localization of Flex applications, you can use resource bundles. Resource bundles are ActionScript objects that provide an interface for accessing localized content defined in a properties file through data binding or ActionScript code. Each bundle your application uses represents a single localization properties file. The properties file is a text file containing a list of localization property keys and their associated values. The key/value pairs are listed in the file in the format key=value, and properties files are saved with a .properties extension.

Localized values for text strings, embedded assets such as images, and references to ActionScript class definitions can all be defined in a properties file. When localizing an application, you define an entry in a properties file for each item in that application that would need to be updated in order for the application to fully support an alternate language. The following example properties file defines the values of several properties in American English:

#Localization resources in American English
pageTitle=Internationalization Demo
language=American English
flag=Embed("assets/usa.png")
borderSkin=ClassReference("skins.en_US.LocalizedSkin")

For each language you want to support, you must create a copy of the properties file that contains values appropriate to that language. If your application must support American English and French, for example, you must create a second properties file containing translated text, a reference to an image of the French flag instead of the American flag, and a reference to the appropriate border skin for the French-language version of the application:

#Localization resources, En Francais
pageTitle=Demo d'internationalisation
language=Francais
flag=Embed("assets/france.png")
borderSkin=ClassReference("skins.fr_FR.LocalizedSkin")

When setting up your properties files, there are several factors to consider—most importantly, the size and complexity of your application. You may wish to create a properties file for each custom component in your application, for example, or for packages of related components that share resources. You may also want to define properties files that are useful on a global scale, such as one containing custom application error messages or commonly used labels (like the ones used on buttons).

No matter how you decide to break up your localization properties, you will need to create a directory structure to organize the files. As illustrated in Figure 24.1, “Directory structure for localization properties files”, a best practice is to create a directory named locale or localization that contains subdirectories named after the locale identifiers. These subdirectories will house all the properties files for a given locale. Adhering to this practice will make it easy to instruct the compiler how to find the properties files.

Figure 24.1. Directory structure for localization properties files

Directory structure for localization properties files

When you build your application, the compiler creates subclasses of the ResourceBundle class for each properties file you define. The easiest way to access items defined in your properties files is through the @Resource directive. Using this method, the compiler substitutes the appropriate property values for the @Resource directive. With @Resource directives, you never actually write code that uses a ResourceBundle instance; the compiler does all the work for you. @Resource directives take two arguments, a bundle identifier and a key, which it uses to look up the appropriate property value in the appropriate properties file. For example, to reference the property name applicationTitle in a localization properties file named localizationProperties.properties, use the following:

@Resource(key='applicationTitle', bundle='localizationProperties')

A more complete example, LocalizationResource.mxml, defines a small application that consumes the localization properties files defined in the two previous code snippets:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/halo">
    <fx:Metadata>
        [ResourceBundle("localizedContent")]
    </fx:Metadata>
    <s:VGroup horizontalCenter="0"
              verticalCenter="0"
              horizontalAlign="center"
              borderSkin="@Resource(key='borderSkin', bundle='localizedContent')">
        <s:Label fontSize="24" text="@Resource(key='pageTitle',
                                               bundle='localizedContent')" />
        <s:Label fontSize="24" text="@Resource(key='language',
                                               bundle='localizedContent')" />
        <s:Image source="@Resource(key='flag', bundle='localizedContent')" />
    </s:VGroup>
</s:Application>

Note that the [ResourceBundle] metadata tells the compiler which bundles are required for a given component. This is important because resource bundles are built at compile time, and all required resources for the supported languages must be compiled into the application SWF.

The compiler must also be configured to build in localization support for the required locales. In Flash Builder, you set these options in the Project Properties dialog box for the Flex project. In the Flex Build Path panel, you define a source path that points to your localization files. If you followed the best practice of using a locale directory, your source path entry will be locale/{locale}, as shown in Figure 24.2, “Localization build path entry”. In addition, you need to identify the locales you wish to support in the Flex Compiler panel’s Additional Compiler Arguments field. For example, to support American English and French, enter -locale en_US,fr_FR (Figure 24.3, “Localization compiler arguments”). When the compiler builds your application and looks for properties files, it substitutes each of the locale identifiers into your source path expression: specifically, locale/en_US for American English properties files and locale/fr_FR for French properties files.

The last step you need to perform before building a localized application is to localize relevant Flex Framework content, such as error messages. You can use the Adobe-provided command-line utility called copylocale to copy each of the relevant files to a new locale. You will have to perform this step once for every locale, but that copy will then be available for use in any project built against that installation of the Flex Framework. You will not have to repeat this step in each project. Note that copylocale will not actually make localized copies of the files for you, but it will let you compile your application. You can find copylocale in the bin subdirectory of the Flex 4 SDK installation. To run it, simply pass in the default locale identifier and the identifier of the locale for which you want to make a copy. For example:

copylocale.exe en_US fr_FR

Prior to Flex 3, you could not change locales at runtime. This meant the only possible approach for building localized applications by using resource bundles was to compile a separate copy of the application for each supported locale. This approach may still be valid in certain cases—for example, when minimal localization is required and the smallest possible SWF file size is desired. To use this technique, set the locale compiler argument to the desired target locale and build your application. The two main advantages to this option are simplicity and small SWF file size, due to the compiler including only one set of localization properties files. To see this technique in action, you can try compiling the LocalizationResource.mxml example application for American English (en_US) or for French (fr_FR) by setting the locale compiler argument appropriately.

Figure 24.2. Localization build path entry

Localization build path entry

Figure 24.3. Localization compiler arguments

Localization compiler arguments

Use the ResourceManager for Localization

Problem

You want to support a small number of locales and either determine the locale programmatically at runtime or allow the user to choose the locale.

Solution

Use the ResourceManager class to support multiple locales and allow the application to change locales at runtime.

Discussion

The ResourceManager class is the Flex programmer’s main ActionScript interface into the resource bundles the compiler creates from localization properties files. It enables you to retrieve various types of resources from resource bundles and provides a mechanism to set the desired locale at runtime. The resource manager is a singleton that manages localization for an entire application. Every class that derives from UIComponent has a protected property named resourceManager that provides a reference to the resource manager singleton.

Although the @Resource directive is convenient for binding localized content into MXML tags, it is much less useful for pure ActionScript components or ActionScript methods that rely on localized resources. In these situations, you’re better off using the resource manager, which provides methods that can be used to access localized data or used as the targets of data-binding expressions in ActionScript or MXML. The following example, excerpted from LocalizationManager.mxml, substitutes ResourceManager methods for the @Resource directives used in the section called “Use a Resource Bundle to Localize an Application”’s LocalizationResource.mxml:

<mx:VBox horizontalCenter="0"
         verticalCenter="0"
         horizontalAlign="center"
         borderSkin="{resourceManager.getClass('localizedContent', 'borderSkin')}">
    <mx:Label fontSize="24" text="{resourceManager.getString('localizedContent',
                                   'pageTitle')}" />
    <mx:Label fontSize="24" text="{resourceManager.getString('localizedContent',
                                   'language')}" />
    <mx:Image source="{resourceManager.getClass('localizedContent', 'flag')}" />
</mx:VBox>

Although the method names vary according to the type of resource you’re retrieving, the arguments here are all similar to the arguments required by an @Resource directive—namely, the resource bundle name and the key value for the desired property.

Using the resource manager to bind localization property values to their targets has an additional benefit. Flex 4 gives you the ability to change the desired locale at runtime; no longer are you forced to build separate localized SWF files for each supported locale. Binding properties to resource methods enables the application to update itself for the desired locale. In the LocalizationManager.mxml example, buttons are provided to let the user switch between English and French:

<mx:HBox>
    <mx:Button label="In English"
               click="resourceManager.localeChain = ['en_US']" />
    <mx:Button label="En Francais"
               click="resourceManager.localeChain = ['fr_FR']" />
</mx:HBox>

In this example, the localeChain property is reset depending on which button the user selects. The localeChain property is an array of strings representing an ordered list of desired locales. This is useful if, for example, the application is going to receive information about the user’s language preferences from the browser, through either the Accept-Language HTTP header or the host operating system’s language preferences. For a user from the United Kingdom, the preferred locale is en_GB, but the user can also accept en_US. When a method call is invoked on the resource manager, it will search for a resource bundle with the specified name in one of the locales defined in the locale chain, in the order they appear. Therefore, an application localized for en_US would be delivered to the user from the United Kingdom if you set localeChain as follows:

resourceManager.localeChain = ["en_GB", "en_US"];

It is a good practice to ensure that the American English locale appears in the property chain somewhere. Many of the Flex Framework components assume that framework localization resources exist, and they will throw an error if they are unable to locate their localized content. Appending en_US to the end of your property chain will help to ensure that the framework classes themselves do not fail because they could not locate localized resources.

When building an application that utilizes the resource manager simply for binding, you do not need to do anything differently compared to using @Resource. However, if your application is going to support multiple locales from which one is selected at runtime, resources for every supported locale must be compiled into the application. You can supply a comma-delimited list of supported locales to the compiler in place of the single locale used in the section called “Use a Resource Bundle to Localize an Application”.

Use Resource Modules for Localization

Problem

You need to support a large number of locales in your application.

Solution

Use resource modules to load only the localization support required by your application at runtime.

Discussion

Localization using resource bundles that are compiled into your application will cause the size of your SWF file to grow with each additional locale you need to support. The overwhelming majority of users will require resources for just one locale, adding up to a lot of dead weight in the download size of your application. Flex 4 adds the ability to compile the resource bundles for a given locale into SWF files called resource modules, which your application can load dynamically at runtime. You then can prompt for or determine programmatically which locale the user prefers, and load only the resource module required to support that user’s locale.

To build resource modules from your localization properties files, you must first determine which resources your application requires. This includes not only resources you define, but also resources required by the Flex Framework. You can use the mxmlc compiler to analyze your application and output a list of required resources. You can do this from Flash Builder by modifying the Additional Compiler Arguments field in the Project Properties dialog box, but it’s easy enough to do from the command line. This will also save you from having to navigate back to the applicable panel in the Project Properties dialog box each time you need to update the list. When you invoke the compiler, you specify the locale and a file where the compiler will write the results of the analysis in addition to the name of the application MXML file:

> mxmlc -locale= -resource-bundle-list=resources.txt ResourceModules.mxml

When the command completes, the contents of the resources.txt output file look like this:

bundles = containers controls core effects localizedContent skins styles

You will use this output to tell the compiler which resource bundles to build into your resource module.

To compile an application that uses resource modules, you must use the command-line compiler. You specify the source path for your localization properties files, the resource bundle list from the previous step, and the name of the resulting SWF file. The compiler then builds resource bundles from your localization properties files and packages them into a SWF file with the specified name. For example, to build resource bundles for the section called “Use Resource Modules for Localization”’s ResourceModules.mxml example, you would use this command:

> mxmlc -locale=en_US -source-path=.,locale/{locale}
  -include-resource-bundles=containers,controls,core,effects,localizedContent,
  skins,styles -output en_US_resources.swf

To compile French resource modules, you would use this command:

> mxmlc -locale=fr_FR -source-path=.,locale/{locale}
  -include-resource-bundles=containers,controls,core,effects,localizedContent,
  skins,styles -output fr_FR_resources.swf

There are several items of interest in these commands. First, you define the target locale by using the locale argument, just as you do when compiling resource bundles into your application. Second, although the source-path argument may look familiar, including the current directory (.) in your source path is important in this case. The example includes an embedded class reference in the localizedContent properties file, and the compiler may not be able to resolve the reference to this class without the root of the application in the source path. Note that this assumes you invoke mxmlc from your project’s source root. Next, the include-resource-bundles argument is filled in based on the list generated earlier. This list is comma-delimited and cannot contain spaces between the delimiter and the bundle names. Finally, you specify a name for the output (e.g., en_US_resources.swf). You can name the output SWF anything you like, but it’s good practice to come up with a naming convention that includes the locale identifier in the filename. This way, you can programmatically determine the name of the resource module your application should load based on a locale identifier.

When you compile resource modules using mxmlc, references to embedded assets such as images will be resolved relative to the location of the localization properties file into which that reference is embedded. Therefore, if your application uses compiled-in resource bundles that have embedded assets defined relative to the project’s source root, these will have to be updated to work as resource modules.

In your application code, you use the resource manager’s loadResourceModule() method: you pass a URL to this method identifying the resource module SWF file you want to use. This method works in a similar fashion to other mechanisms for loading ActionScript objects at runtime, such as SWFLoader or conventional modules. A request is made to the server for the required SWF, which is then downloaded by the browser. Requests for resources from other domains require a cross-domain policy file. You must wait until the resource module has been loaded into your application before you can make use of it. When the resource module is ready for use, a ResourceEvent will be dispatched. You can listen for these events by defining a listener for ResourceEvent.COMPLETE events. The loadResourceModule() method returns a reference to an object that implements the IEventDispatcher interface that you use to register listeners for the complete event. The following code is excerpted from the ResourceModules.mxml example and demonstrates how a resource module is loaded and utilized:

import mx.events.ResourceEvent;
import mx.resources.ResourceManager;

private var selectedLocale:String;

private function setAppLocale(locale:String):void
{
    this.selectedLocale = locale;
    if (resourceManager.getLocales().indexOf(locale) == −1)
    {
        var dispatcher:IEventDispatcher =
                resourceManager.loadResourceModule(locale + "_resources.swf");
        dispatcher.addEventListener(ResourceEvent.COMPLETE, onResourceLoaded);
    }
    else
    {
        onResourceLoaded(null);
    }
}

private function onResourceLoaded(e:ResourceEvent):void
{
    resourceManager.localeChain = [this.selectedLocale];
    views.selectedIndex = 1;

    contentBackground.setStyle("borderSkin",
            resourceManager.getClass('localizedContent', 'borderSkin'));
    contentBackground.invalidateDisplayList();
    contentBackground.validateNow();
}

In this example, the user is prompted to choose between American English and French. When the user selects a language, the setAppLocale() function is called to load the required resource module. This method first checks whether resources for the requested locale have already been loaded by searching the result of the resource manager’s getLocales() method. It is a good practice to test whether you currently have the resources required in order to save the overhead of requesting and loading resources that are already present. If the requested locale isn’t already loaded, a call is made to the loadResourceModule() method to retrieve it, and a listener for the complete event is registered so you know when the resource module has been loaded and is ready for use.

In response to the complete event, the application sets the localeChain property to make use of the newly loaded resource module. Of note in this method are the three method calls on the contentBackground object. Style settings are not bound in Flex, so objects referencing style properties from resource modules must be updated programmatically in order to pick up changes to their styling properties.

The loadResourceModule() method can take several optional parameters in addition to the resource module URL. If your application loads multiple resource modules, you will want to load all but the last with the update parameter set to false. This will save overhead by not running the resource manager’s update routines repeatedly.

Support Input Method Editor (IME) Devices

Problem

You need to distribute your application in a language that uses multibyte characters, such as Japanese, Chinese, or Korean.

Solution

Use the Capabilities class to detect an input method editor and the IME class to control how it interacts with your Flex application.

Discussion

Far Eastern languages such as Chinese represent words with single ideograms, rather than with combinations of letters as in Latin languages. In Latin languages, the number of individual characters is limited and they can easily be mapped onto a keyboard with a limited number of keys. It would be impossible to do the same for Far Eastern languages, as this would require thousands of keys. Input method editors (IMEs) are software tools that allow characters to be composed with multiple keystrokes. An IME runs at the operating system level, external to the Flash Player.

The Capabilities class has a property called hasIME that you can use to determine whether the user has an IME installed. You can use the flash.system.IME object to test whether the IME is enabled and what conversion mode it is set to. The following example tests for an IME and, if it finds one, starts the IME and sets the required conversion mode:

private function detectIME():void
{
    if (Capabilities.hasIME == true)
    {
        output.text = "Your system has an IME installed.\n";
        if (flash.system.IME.enabled == true)
        {
            output.text += "Your IME is enabled. and set to " +
                    flash.system.IME.conversionMode;
        }
        else
        {
            output.text += "Your IME is disabled\n";
            try
            {
                flash.system.IME.enabled = true;
                flash.system.IME.conversionMode =
                        IMEConversionMode.JAPANESE_HIRAGANA;
                output.text += "Your IME has been enabled successfully";
            }
            catch (e:Error)
            {
                output.text +="Your IME could not be enabled.\n"
            }
        }
    }
    else
        output.text = "You do not have an IME installed.\n";
}

When trying to manipulate the IME settings, you should always use a try...catch block. If the current IME does not support the specified settings, the call will fail.

You may wish to disable the IME in some cases, such as for a text field that expects numeric input. If the IME is inappropriate for a given data entry component, you can trigger a function to disable it when that component gets focus and re-enable it after the component loses focus:

<fx:Script>
    <[[
        private function enableIME(enable:Boolean):void
        {
            if (Capabilities.hasIME)
            {
                try
                {
                    flash.system.IME.enabled = enable;
                    trace("IME " + (enable ? "enable" : "disable"));
                }
                catch (e:Error)
                {
                    Alert.show("Could not " (enable ? "enable" : "disable") +
                               " IME");
                }
            }
        }
    ]]>
</fx:Script>
<s:VGroup horizontalCenter="0" verticalCenter="0">
    <s:TextInput id="numericInput" focusIn="enableIME(false)"
                 focusOut="enableIME(true)" />
    <s:TextInput id="textInput" />
</s:VGroup>

If you want to know when a user has composed a character, you can listen for events on the System.ime object:

System.ime.addEventListener(IMEEvent.IME_COMPOSITION, onComposition);

Detect a Screen Reader

Problem

You must provide support for visually impaired users and would like to customize your application for screen-reader users.

Solution

Use the active static property of the Accessibility class to detect a screen reader.

Discussion

Rich media capabilities and a cinematic user experience are hallmarks of a Rich Internet Application. Unfortunately, these capabilities can make using a Flex application difficult for visually impaired users. Screen-reader support is important for visually impaired users, and may in fact be their only method of interacting with your application. If accommodating visually impaired users is a requirement, you may wish to alter the user experience specifically for screen-reader users. The active property of the Accessibility class can be used to test whether a user is using a screen reader. The following code block, excerpted from ScreenReader.mxml, uses Accessibility.active to determine whether an animation should play:

private function showNextPage():void
{
    if (Accessibility.active == false)
    {
        page2.visible = true;
        pageChangeAnimation.play();
    }
    else
    {
        page1.visible = false;
        page2.alpha = 1;
    }
}

Create a Tabbing Reading Order for Accessibility

Problem

You must support users who may have difficulty using a pointing device.

Solution

Define a tab order for components in your application so that the user may navigate through the application without using a pointing device.

Discussion

Tab order is an important usability concern in an application. It enables users to effectively navigate through the application without having to switch between the keyboard and a pointing device unnecessarily. For users with impairments that make using a pointing device difficult or impossible, tab order is a necessity. You can specify the tab order in your components by setting the tabIndex property of each object in the component. The following example, saved as TabOrder.mxml, sets a tab order so a user can easily navigate through an address form without using a mouse:

<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
               xmlns:s="library://ns.adobe.com/flex/spark"
               xmlns:mx="library://ns.adobe.com/flex/halo"
               creationComplete="firstName.setFocus()">
    <s: VGroup width="228" height="215" x="50" y="50" backgroundColor="#FFFFFF">
        <s:Label x="10" y="10" text="First Name" tabIndex="1" />
        <s:TextInput x="10" y="36" width="100" id="firstName" tabIndex="2"/>
        <s:Label x="118" y="10" text="Last Name" tabIndex="3" />
        <s:TextInput x="118" y="36" width="100" id="lastName" tabIndex="4"/>
        <s:Label x="10" y="69" text="Address" tabIndex="5" />
        <s:TextInput x="10" y="95" width="208" id="address" tabIndex="6"/>
        <s:Label x="10" y="125" text="City" tabIndex="7"/>
        <s:TextInput x="10" y="151" width="100" id="city" tabIndex="8"/>
        <s:Label x="118" y="125" text="State" tabIndex="9"/>
        <s:TextInput x="118" y="151" width="34" id="state" tabIndex="10"/>
        <s:Label x="160" y="125" text="Zip" tabIndex="11"/>
        <s:TextInput x="160" y="151" width="58" id="zip" tabIndex="12"/>
        <s:Button x="153" y="181" label="Submit" id="submit" tabIndex="13"/>
    </s:VGroup>
</mx:Application>

Note that in this example, the tabIndex property is set for all labels as well as the text inputs and buttons, even though the labels cannot receive focus. For users who require a screen reader, the tab order also dictates the order in which the screen reader will describe items on the page. When considering screen-reader users, it is important that you set the tabIndex property for all accessible components, not just those that can receive focus. Any items that do not have their tabIndex set will be placed at the end of the tab order, which could confuse screen-reader users when those items are read out of order compared to the visual layout.

Print Selected Items in an Application

Problem

You need to create printed output from an application.

Solution

Use the classes in the mx.printing package to define, format, and produce printed output.

Discussion

The mx.printing package implements several classes used to produce printed output. For example, the FlexPrintJob class defines a print job, adds items to the job, and sends the job to the printer. The following example, saved in BasicPrintJob.mxml, creates a print job, adds two pages of output, and sends the job to the printer:

<s:SkinnableContainer xmlns:mx="http://www.adobe.com/2006/mxml"
                      width="400" height="300">
    <fx:Script>
        <![CDATA[
            import mx.printing.FlexPrintJob;

            public function print():void
            {
                var printJob:FlexPrintJob = new FlexPrintJob();
                if (printJob.start())
                {
                    printJob.addObject(pageContainer1);
                    printJob.addObject(pageContainer2);
                    printJob.send();
                }
            }
        ]]>
    </fx:Script>
    <s:VGroup width="380" height="260" verticalCenter="-20" horizontalCenter="0">
        <s:VGroup id="pageContainer1">
            <s:Label text="Page 1" />
            <s:TextArea id="page1" width="100%" height="100%" />
        </s:VGroup>
        <s:VGroup id="pageContainer2">
            <mx:Label text="page 2" />
            <mx:TextArea id="page2" width="100%" height="100%" />
        </s:VGroup>
    </s:VGroup>
    <s:Button bottom="5" right="10" label="Print" click="print();" />

When the start() method is called, the operating system displays the Print dialog box. Execution is suspended until the user has finished configuring the print job. If the user decides to cancel the print job, the start() method returns false. Otherwise, the function calls the addObject() method to add the text area to the print job and then calls the send() method to send the job to the printer.

Each time you call addObject(), the item and all of its children are placed on a new page. In the printed output generated by this example, the page labels and text inputs contained in pageContainer1 and pageContainer2 are sent to the printer on separate pages.

The addObject() method also accepts an optional parameter that tells the print job how to scale the added item. If an item is too big to fit, the print job will render it on multiple pages. By default, the item will be scaled to the width of the page, but several other options are available. These options are defined as static constants on the FlexPrintJobScaleType class. You may, for example, want to scale a column chart so that it fits vertically within a single page, ensuring that the value of each column can be read on a single page:

public function print():void
{
    if (printJob.start())
    {
        printJob.addObject(columnChart, FlexPrintJobScaleType.MATCH_HEIGHT);
        printJob.send();
    }
}

If the chart is too wide to fit within a single page, the excess will be printed on a new page. An example, called ScaleExample.mxml, has been provided to demonstrate the effects of the various scale types. This file is available on the book’s website, along with all the others.

Format Application Content for Printing

Problem

You want your application to produce output specifically formatted for printing.

Solution

Build custom print renderer components to format output specifically for printing.

Discussion

Often, the output you want to generate for printing is different from what is displayed to the user in the application. You may wish to create printable versions of application objects or generate reports of data not shown to the user through the application. This is accomplished by using a print renderer, a component you create specifically for generating printed output.

In the BasicPrintJob.mxml example from the section called “Print Selected Items in an Application”, for instance, you may not want to print the page label or the text input control’s border. Also, you probably want the entered text printed as if it had been created in a word processor, filling the width of the page without scaling the text and continuing on to the next page when the end of the page is reached. To format a text block for printing, use a component like the one shown here:

<s:SkinnableContainer xmlns:mx="http://www.adobe.com/2006/mxml"
                      backgroundColor="0xffffff">
    <fx:String id="textToPrint" />
    <s:TextArea width="100%" text="{textToPrint}" />
</s:SkinnableContainer>

When you use a print renderer to format output, you must first add the renderer to a display list in order for Flex to lay out the visual aspects of the component. Take care when considering where to add the component. Unintended consequences, such as shifts in layout or unintended scroll bars, can occur in some cases. In the following code the renderer is added to the parent application’s display list in order to avoid the appearance of scroll bars:

public function print():void
{
    var printJob:FlexPrintJob = new FlexPrintJob();
    if (printJob.start())
    {
        var printRenderer:BasicTextRenderer = new BasicTextRenderer();
        printRenderer.width = printJob.pageWidth;
        printRenderer.textToPrint = page1.text;
        printRenderer.visible = false;
        FlexGlobals.topLevelApplication.addChild(printRenderer);
        printJob.addObject(printRenderer);
        printJob.send();
        FlexGlobals.topLevelApplication.removeChild(printRenderer);
    }
}

Also of note in this example is the use of the pageWidth property of the printJob object. Both the pageWidth and pageHeight properties are set when the start() method has returned. When writing a print renderer component, it is important to pay attention to these properties when sizing components. By using these properties, you can ensure that your renderer will work in both portrait and landscape mode, and with varying paper sizes and types of printers.

See Also

the section called “Print Selected Items in an Application”

Control Printing of Unknown-Length Content over Multiple Pages

Problem

You need to control the layout of printed output over multiple pages, but you don’t know how much data you will be printing or the sizes of the components you need to lay out.

Solution

Use the PrintDataGrid component to control how data is printed over multiple pages when printing tabular data.

Discussion

If you have tabular data, such as a spreadsheet-style report, you can use the PrintDataGrid component to format that data for multipage output. The PrintDataGrid component is a specialized data grid that is designed to format printed data across multiple pages and can be used to control a variety of types of repetitive multipage output. The following example, from MultipageDataGrid.mxml, utilizes a PrintDataGrid to format a report for printing:

public function print():void
{
    var printJob:FlexPrintJob = new FlexPrintJob();
    if (printJob.start())
    {
        var printGrid:PrintDataGrid = new PrintDataGrid();
        printGrid.width = printJob.pageWidth;
        printGrid.height = printJob.pageHeight;
        printGrid.columns = populationGrid.columns;
        printGrid.dataProvider = populationData.state;
        printGrid.visible = false;
        FlexGlobals.topLevelApplication.addChild(printGrid);
        printJob.addObject(printGrid);
        while (printGrid.validNextPage)
        {
            printGrid.nextPage();
            printJob.addObject(printGrid);
        }
        printJob.send();
        parentApplication.removeChild(printGrid);
    }
}

When using a PrintDataGrid, you set its size to match your page size. Adding the grid to the print job will add the first page. You can test whether additional pages of data exist by using the validNextPage property, and you can advance to the next page of output by using the nextPage() method.

Used creatively, the PrintDataGrid component can help format many kinds of output. PrintDataGrid isn’t restricted to printing only tabular text; it can be used in combination with an item renderer to produce repetitive layouts of things like charts, images, or complex components. In the following example, GridSquares.mxml, the PrintDataGrid is used in combination with an item renderer to produce a collection of red squares identical to the ManualMultiPage.mxml example:

public function print(itemSize:int, itemCount:int):void
{
    var printData:Array = new Array();
    for (var i:int = 0;  i < itemCount;  i++)
    {
        printData.push(itemSize);
    }

    var column:DataGridColumn = new DataGridColumn();
    column.headerText = "";
    column.itemRenderer = new ClassFactory(SquareRenderer);

    var printGrid:PrintDataGrid = new PrintDataGrid();
    printGrid.showHeaders = false;
    printGrid.visible = false;
    printGrid.setStyle("horizontalGridLines", false);
    printGrid.setStyle("verticalGridLines", false);
    printGrid.setStyle("borderStyle", "none");
    printGrid.columns = [column];
    printGrid.dataProvider = printData;
    FlexGlobals.topLevelApplication.addChild(printGrid);

    var printJob:FlexPrintJob = new FlexPrintJob();
    if (printJob.start())
    {
        printGrid.width = printJob.pageWidth;
        printGrid.height = printJob.pageHeight;
        printJob.addObject(printGrid);
        while (printGrid.validNextPage)
        {
            printGrid.nextPage();
            printJob.addObject(printGrid);
        }
        printJob.send();
    }

    parentApplication.removeChild(printGrid);
}

Add a Header and a Footer When Printing

Problem

You need to produce printed output with headers and footers.

Solution

Create a print renderer component to control the page layout.

Discussion

Combining a print renderer with a PrintDataGrid enables much finer control over your printed layouts than the PrintDataGrid can provide itself. A common task is to add headers and footers to printed pages. This technique involves manipulating whether the header and footer are included in the layout and testing the results by using the validNextPage() property of the PrintDataGrid. The following code, HeaderFooterPrintRenderer.mxml, defines a print renderer that produces multipage output with header and footer areas included where appropriate:

<?xml version="1.0" encoding="utf-8"?>
<s:VGroup xmlns:fx="http://ns.adobe.com/mxml/2009"
          xmlns:s="library://ns.adobe.com/flex/spark"
          xmlns:mx="library://ns.adobe.com/flex/halo"
          horizontalAlign="center">

    <fx:Declarations>
        <mx:DateFormatter id="formatter" formatString="M/D/YYYY" />
        <mx:DateFormatter id="format" formatString="m/d/yyyy" />
    </fx:Declarations>

    <fx:Script>
        <![CDATA[

            public function startJob():void
            {
                // try to print this on a single page
                header.visible = true;
                header.includeInLayout = true;
                footer.visible = true;
                footer.includeInLayout = true;

                this.validateNow();

                if (printGrid.validNextPage)
                {
                    // the grid is too big to fit on a single page
                    footer.visible = false;
                    footer.visible = false;

                    this.validateNow();
                }
            }

            public function nextPage():Boolean
            {
                header.visible = false;
                header.includeInLayout = false;

                printGrid.nextPage();

                footer.visible = !printGrid.validNextPage;
                footer.includeInLayout = !printGrid.validNextPage;

                this.validateNow();

                return printGrid.validNextPage;
            }
        ]]>
    </fx:Script>
    <s:Panel id="header" height="80" width="100%">
        <s:Label text="Population by State"
                 fontSize="24"
                 color="0x666666"
                 horizontalCenter="0"
                 verticalCenter="0"
                 width="100%"
                 textAlign="center" />
    </s:Panel>
    <s:Panel height="100%" width="80%">
        <mx:PrintDataGrid id="printGrid" width="100%" height="100%">
            <mx:columns>
                <mx:DataGridColumn dataField="@name"
                                   headerText="State" />
                <mx:DataGridColumn dataField="@population"
                                   headerText="Population"/>
            </mx:columns>
        </mx:PrintDataGrid>
    </s:Panel>
    <s:Group id="footer" height="80" width="100%">
        <s:Label text="{formatter.format(new Date())}"
                 left="20" bottom="5" />
    </s:Group>
</s:VGroup>

This component defines a header containing the report title and a footer that displays the date the report was printed. The startJob() method initializes the print layout to the appropriate first-page layout. It first tries to lay out the page as if all data will fit on a single page. After a call to the validateNow() method forces the layout of the component to occur, you can test whether the report will fit within one page by testing the PrintDataGrid’s validNextPage property. If the value is false, the report will fit. If not, the layout is adjusted to hide the footer and the layout is updated again. At this point, whether the report is a single page or multiple pages, the first page is ready to be added to the print job. If the report does require multiple pages, the nextPage() method will prepare the layout appropriately. It hides the header (it will never be used except on the first page) and enables the footer when appropriate.

Building the page-layout intelligence into the renderer greatly simplifies the actual print routine. The following code block, taken from HeaderFooter.mxml, demonstrates the use of the example print renderer in an application:

public function print():void
{
    var printJob:FlexPrintJob = new FlexPrintJob();
    if (printJob.start())
    {
        var printRenderer:HeaderFooterPrintRenderer =
                new HeaderFooterPrintRenderer();

        printRenderer.visible = false;
        this.addChild(printRenderer);
        printRenderer.width = printJob.pageWidth;
        printRenderer.height = printJob.pageHeight;
        printRenderer.dataProvider = populationData.state;
        printRenderer.startJob()

        do
        {
            printJob.addObject(printRenderer);
        }
        while (printRenderer.nextPage());

        // send the last page
        printJob.addObject(printRenderer);
        printJob.send();

        this.removeChild(printRenderer);
    }
}

The print() method begins a print job and sets up a print renderer in a similar fashion to previous examples. This portion of the code finishes by calling the startJob() method on the print renderer, which lays out the renderer for the first page. In the next section of the method, the do...while block continues to add pages to the print job until the nextPage() method returns false, indicating there are no more pages to lay out. However, because the do...while block invokes the nextPage() method at the end of the block, the last page will not yet be added to the print job when the loop completes. The print() function therefore queues the last page manually, sends the print job, and removes the renderer from the display list.

If you enjoyed this excerpt, buy a copy of Flex 4 Cookbook.

Copyright © 2009 O'Reilly Media, Inc.