28.1 Working with Focus


Prior to SDK 1.4, focus issues in Java were complicated and not adequately implemented. The problems were deeper than bugs in the code of which there were certainly plenty, many due to inconsistencies between platforms. The design itself was fundamentally flawed.[1]

[1] For more details about the deficiencies and how they have been remedied, see Sun's article, "The AWT Focus Subsystem for Merlin" at http://java.sun.com/products/jfc/tsc/articles/merlin/focus/.

Because of these deep changes, the "right way" to do things now differs substantially from previous versions of Java. To avoid confusion, we discuss only the focus model introduced in 1.4. Developers who work with earlier releases should refer to the book's web site (http://www.oreilly.com/catalog/jswing2) for the first edition's version of this section.

Here are some highlights of changes in the focus system in 1.4:

  • FocusManager has been deprecated and replaced by KeyboardFocusManager.

  • It's now possible to learn which component currently has the focus.

  • Lightweight children of Window (not just Frame or Dialog) are able to receive keyboard input.

  • When focus changes, the component gaining focus can find out which one lost it, and vice versa.

  • There is far less platform-dependent code; it has been replaced by a very extensible public API in AWT, with many levels where custom logic can be plugged in. Heavyweight and lightweight focus is much better integrated.

  • Components can lose focus temporarily (e.g., to a scrollbar).

  • It's a lot easier to work with.

The net result of all these changes is that the system tends to work the way you want it to, and even when you want to add fancy new features, doing so requires less effort and simpler code.

28.1.1 Overview

Focus specifies the component that receives keyboard events when the user presses keys on the keyboard. In managing focus, it is also important to define how and when focus transfers from one component to another, and the order in which components are traversed when focus moves forward or backward. These concepts are strongly linked: if a component can receive focus at all, then it participates in a focus traversal order, and vice versa.

In Swing, the KeyboardFocusManager keeps track of these relationships and interacts appropriately with components and the keyboard. It registers itself as a KeyEventDispatcher, monitoring the stream of keyboard events and deciding how to respond to them. (Your own classes can implement this interface if they need to watch, and potentially handle, all keyboard events regardless of focus.)

The KeyboardFocusManager uses a FocusTraversalPolicy to determine the traversal order as focus moves between components. A number of standard policy implementations are provided. If you don't explicitly request one, the LayoutFocusPolicy is installed by the standard layout managers and provides behavior that is backward-compatible with previous editions of Java. It examines the positions and sizes of the components within the container and provides traversal that is consistent with the ComponentOrientation (for left-to-right containers, focus starts at the top left, moving along the top row until the right edge is reached, and then jumps to the left of the next row down). If you have special traversal needs, you can implement your own FocusTraversalPolicy and install it in a container, as we'll demonstrate later in this section.

28.1.1.1 Focus traversal

How does Swing know when it's time to move focus from one component to another? This almost always occurs in response to user actions. The most straightforward case involves the user clicking on a component with the mouse. If that component is enabled and focusable, it receives focus immediately. There are no tricky rules.

Focus traversal policies really come into play when users indicate, via the keyboard, that they're finished with the current component and are ready to move on to the next one. The specific keys used to signal such intentions are up to individual platforms and L&F implementations, but usually, Tab indicates a desire to move to the next component, while Shift-Tab requests a move back to the previous component. Because users enter substantial amounts of text into TextArea components and may want to insert a Tab character to indent a paragraph, Ctrl-Tab is commonly used to move focus forward out of a TextArea, and Ctrl-Shift-Tab is used to move backward. The Component class provides a set of properties to define and manipulate these focus traversal keys so that each component can define its own behavior. The fewer differences there are, of course, the more comfortable and productive the environment is for users.

The traversal policy is also consulted when a window is first shown to determine which component starts out with the focus (unless the application has already explicitly assigned focus). If the frame of an already visible window is clicked by the user, the component that previously had focus within that window generally gets it back, without intervention by any traversal policy.

There may be other times when your application wants to transfer focus. For example, if you have a serial-number entry dialog containing a series of small text fields, to visually separate each chunk of characters in the serial number, it would be friendly to automatically jump to the next field as each is filled. As you might expect, KeyboardFocusManager provides methods your code can invoke to move focus in any way that the user might request via the keyboard, and components can be explicitly given focus.

28.1.1.2 Validation

The focus traversal mechanism often interacts closely with application logic when it comes to validating user input. The point at which users are moving on from one of your entry fields is an excellent time to make sure that what they've entered is complete and valid. Listening for the FOCUS_LOST event provides just this capability. If your code finds an incorrect value, you display an error and set focus on the problem component or, less intrusively but perhaps more cryptically, simply beep and keep the focus from leaving by using a VetoableChangeListener. (There are some complexities involved in using this approach safely, though; you should study the relevant Javadoc carefully to avoid deadlocks and other pitfalls.) In fact, you no longer need to worry about these details: since SDK 1.3, Swing provides a convenient built-in facility for verifying user input when a component loses focus. The inputVerifier property of JComponent is discussed in Chapter 3, and the details of the mechanism are explained in Chapter 20.

Of course, this approach applies only to fields that can be validated on their own. There may also be important cross-field constraints that you'll validate only when the user has filled in an entire form and is trying to take action with it. Such code would not rely on the focus mechanism at all, being invoked in response to a button press or menu choice. At the opposite extreme, you may be able to validate some input on a key-by-key basis, screening out keystrokes that don't make sense, as in a numeric-only field.

28.1.1.3 Temporary focus changes

Swing makes a distinction between permanent and temporary focus changes. All of the discussion so far has been about permanent changes of focus, in which users have moved on to a different component and won't be coming back unless they want to (hence the appropriateness of validation). Sometimes, however, focus is expected to return to a component in a short amount of time, such as when a popup menu is shown or a scrollbar is dragged. In such cases, validation would be premature, as the user hasn't finished interacting with the component. The focus-related events provided by Swing allow your code to distinguish between these situations and react appropriately.

28.1.1.4 Focus cycles

As the user tabs between components, not every component on the screen gets focus. At the most obvious level, focus remains "trapped" within the top-level window in which it began (this also applies to JInternalFrames being used in a JDesktopPane) until the user or program explicitly moves it somewhere else. These containers are "focus cycle roots" because the components within them form a focus cycle, a group of components through which focus cycles without leaving. If you have a special situation in which you'd like to create a focus cycle root within another type of component, you can override the isFocusCycleRoot( ) method inherited from Component and return true. (As usual, you should have a good reason for doing this because it restricts the user's choices, probably in an unexpected way. There would need to be a strong visual cue about the relatedness of your enclosed components, to the exclusion of all others, in order for it to make sense.) Figure 28-1 shows the effect this property has on focus traversal.

Figure 28-1. Focus cycle root behavior
figs/swng2.2801.gif

In addition to the focus traversal keys, Swing lets components provide keys to enable the user to jump out of a focus cycle. Even though the support for these keys exists, and components may define them, none of the built-in components define any keys to perform these functions. Of course, there are also methods in KeyboardFocusManager that your application can use to request this kind of focus traversal.

28.1.2 Using Focus Properties

Here is an example that demonstrates some of the main focus properties:

// FocusExample.java // import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.*; public class FocusExample extends JFrame {     public FocusExample( ) {         super("Focus Example");         setDefaultCloseOperation(EXIT_ON_CLOSE);         MyPanel mypanel = new MyPanel( );         JButton  button1 = new JButton("One");         JButton  button2 = new JButton("Two");         JButton  button3 = new JButton("Three");         JButton button4 = new JButton("Four");         JButton button5 = new MyButton("Five*");         JButton  button6 = new MyButton("Six*");         JButton  button7 = new JButton("Seven");         mypanel.add(button2);         mypanel.add(button3);         JInternalFrame frame1 = new JInternalFrame("Internal Frame 1",                                                    true, true, true, true);           frame1.setBackground(Color.lightGray);         frame1.getContentPane( ).setLayout(new GridLayout(2, 3));         frame1.setSize(300, 200);         frame1.getContentPane( ).add(button1);         frame1.getContentPane( ).add(mypanel);         frame1.getContentPane( ).add(button4);         frame1.getContentPane( ).add(button5);         frame1.getContentPane( ).add(button6);         frame1.getContentPane( ).add(button7);         JDesktopPane desktop = new JDesktopPane( );         desktop.add(frame1, new Integer(1));         desktop.setOpaque(true);         // Now set up the user interface window.         Container contentPane = getContentPane( );         contentPane.add(desktop, BorderLayout.CENTER);         setSize(new Dimension(400, 300));         frame1.setVisible(true);         setVisible(true);     }     public static void main(String[] args) {         new FocusExample( );     }     class MyButton extends JButton {         public MyButton(String s) { super(s); }         public boolean isFocusable( ) { return false; }     }     class MyPanel extends JPanel {         public MyPanel( ) {             super(true);             java.util.Set upKeys = new java.util.HashSet(1);             upKeys.add(AWTKeyStroke.getAWTKeyStroke(KeyEvent.VK_UP, 0));             setFocusTraversalKeys(KeyboardFocusManager.UP_CYCLE_TRAVERSAL_KEYS,                                    upKeys);         }         public boolean isFocusCycleRoot( ) { return true; }     } }

This program creates the seven buttons in the internal frame shown in Figure 28-2. The source shows several things worth noting. First, buttons Two and Three are contained in a special panel, called MyPanel, that has declared itself as the root of its own focus cycle. Hence, if you click on (or tab into) either of these two buttons and repeatedly press Tab, you'll see that the focus simply bounces back and forth between them. MyPanel also defines the up arrow key to move up out of the focus cycle root. (At press time, the released version of 1.4 seems to require you to hit the key twice before Tab takes you to a button outside the focus cycle; it's not clear why this would be desirable, and it may just be a bug that nobody's noticed because none of the standard components provide such keystrokes.)

Figure 28-2. FocusExample in action
figs/swng2.2802.gif

Buttons Five and Six are both of type MyButton, which sets the focusable property to false so they cannot receive focus (this is visually indicated in the example by the "*" after their names). Clicking on them leaves focus where it was, and pressing Tab when button Four has focus (as in the figure) jumps right to Seven.

28.1.3 The KeyboardFocusManager Class

The Swing keyboard focus manager is responsible for determining how the focus is transferred from one component to the next. It provides methods that allow your code to find out which component currently has focus, request a change in focus, and set up a different policy for focus traversal. In addition to asking about focus on an ad hoc basis, you can register a PropertyChangeListener to be notified about changes as they occur. The keyboard focus manager dispatches all FocusEvents, all WindowEvents that are related to focus, and all KeyEvents.

KeyboardFocusManager is an abstract class; Swing provides a default implementation, DefaultKeyboardFocusManager. Although it is possible to replace this default if you need to control focus at a very low level, doing so is extremely complicated and requires detailed knowledge of each platform's peer focus system. Luckily, this is almost never necessary because of the flexibility provided by FocusTraversalPolicy implementations, KeyEventDispatchers, VetoableChangeListeners, and the like, which enable you to manipulate focus on a higher level, where it relates to your immediate needs.

In addition to tracking which component is focused and receives KeyEvents, this class keeps track of the active and focused Windows (for a window to be focused means that it either contains, or is itself, the focus owner). Only a Frame or Dialog can be the focused window. A Window is active if it is focused, or if it is the first Frame or Dialog encountered while following the owner chain upward from the focused Window.

Although any given thread interacts only with a single KeyboardFocusManager, keep in mind that, in a web browser, applets from different code bases might be partitioned into separate, mutually inaccessible contexts. In such situations, each context has its own KeyboardFocusManager. Even in implementations with multiple focus managers, however, there is never more than one focused or active Window per ClassLoader.

Many of the methods in KeyboardFocusManager are intended to be used only by concrete implementations that need to access low-level internals in order to correctly manage the details of focus. As such, we'll omit discussion of them and cover the interface that is useful in more typical applications. In the unlikely event you are implementing your own manager, refer to the class Javadoc for the rest of the details. Since the new, pluggable focus architecture makes it possible to easily achieve most goals that used to require writing custom focus managers, few developers will face that task.

28.1.3.1 Constants
public static final int BACKWARD_TRAVERSAL_KEYS
public static final int DOWN_CYCLE_TRAVERSAL_KEYS
public static final int FORWARD_TRAVERSAL_KEYS
public static final int UP_CYCLE_TRAVERSAL_KEYS

These constants are used in methods that manipulate the lists of keys with which the user can request changes of focus. They apply to both the defaultFocusTra-versalKeys property of this class and the focusTraversalKeys property of the Component class.

28.1.3.2 Constructor
public KeyboardFocusManager( )

Initialize the KeyboardFocusManager class. Since it's an abstract class, only subclasses can call this method directly. In any case, you're more likely to use the getCurrentKeyboardFocusManager( ) method in everyday coding.

28.1.3.3 Methods
public static KeyboardFocusManager getCurrentKeyboardFocusManager( )

Retrieve the current focus manager for the context associated with the calling thread.

public static void setCurrentKeyboardFocusManager(FocusManager newManager)

Establish a new focus manager for the calling thread's context.

public void clearGlobalFocusOwner( )

Cause there to be no focus owner (both within Java and in the native windowing environment). Upon completing this operation, all keyboard input from the user is discarded until the user or the application selects a new component to receive focus.

public void focusNextComponent( )

Move focus to the component following the current focus owner according to the focus traversal policy.

public void focusPreviousComponent( )

Move focus to the component preceding the current focus owner according to the focus traversal policy.

public void upFocusCycle( )

Move focus up one focus cycle from the current focus owner.

public void downFocusCycle( )

Move focus down one focus cycle from the current focus owner. Has no effect if the current focus owner is not a container that is a focus cycle root.

28.1.3.4 Properties

Several properties allow your application to learn about and manipulate the focus state. They are shown in Table 28-1. Since the class has been available only since SDK 1.4, all properties are new.

Table 28-1. KeyboardFocusManager properties

Property

Data type

get

set

Default value

activeWindowc

Window

·

   

currentFocusCycleRootb, n

Container

·

   

defaultFocusTraversalKeysb, i

Set of AWTKeystrokes

·

·

See discussion in Section 28.1.1.1

defaultFocusTraversalPolicyb

FocusTraversalPolicy

·

·

LayoutFocusTraversalPol-icy

focusedWindowc

Window

·

   

focusOwnerc

Component

·

   

permanentFocusOwnerc

Component

·

   

bbound, cconstrained, iindexed (using the TRAVERSAL_KEYS constants), nnot for general client use

The focusOwner property tracks the focus owner if there is one and if it belongs to the same context as the calling thread. Otherwise, the property is null. Similarly, the permanentFocusOwner property tracks the permanent focus owner, which is the same as the focusOwner unless a component (such as a pop up or scrollbar) has been temporarily given focus, in which case this property instead returns the component to which focus will shortly return.

The focusedWindow property tracks the Window that contains the focus owner if there is one and if it belongs to the same context as the calling thread. Similarly, the activeWindow property tracks the active Window, which is the same as the focused window unless it is not a Frame or a Dialog. If the focused window is some other class, the active window is the first Frame or Dialog that owns it, starting with the focused window and following the owner chain.

The defaultFocusTraversalPolicy property determines the focus traversal policy that is adopted by top-level components during initialization. Changing this affects future components; ones that have already been created retain their existing policies.

The defaultFocusTraversalKeys property determines the keys with which the user may, by default, request a specified focus traversal operation. The "index" for this property identifies the type of focus traversal that is of interest and must be one of the four constants listed above. The property value is a Set of AWTKeyStrokes to be used by all Windows that have not explicitly specified a different set and is inherited recursively by any child Component of these windows (again, unless that component has explicitly provided its own overriding set). (See Section 28.1.2 for an example of manipulating these sets of keys.) The elements of the supplied Set must all be AWTKeyStrokes and must not represent any KEY_TYPED events. None of the keystrokes may already be in use by a different default focus traversal operation.

The currentFocusCycleRoot property is intended for internal use by classes implementing focus management and is listed here only because your listeners might receive property-change events about it since it's a bound property.

28.1.3.5 Listener lists

The keyboard focus manager provides several listener lists. Rather than detailing all the methods used to manipulate and query these lists (which are by now quite familiar), here is a high-level description of the purpose of each list:

PropertyChangeListener

Allows clients to be notified of changes to any bound properties of the focus manager. Since all the properties are either bound or constrained, they all generate notifications.

VetoableChangeListener

Allows clients to prevent impending changes to the constrained properties of the focus manager. It might seem strange to see properties in the table marked as constrained without any visible ability to "set" the properties. Even though there is no mutator method provided by this class to explicitly set, for example, the focused component, the focusNextComponent( ) method and its kin do affect these properties, and their operation can be vetoed. There is also a Component method to request focus, and listeners can veto that request. They can also veto changes in focus initiated by the user.

KeyEventDispatcher

Enables clients to access the stream of KeyEvents before they are processed. Each KeyEventDispatcher will be given a chance to examine the event (in the order in which the dispatchers were added) and may halt further processing of the event. Only if none of the registered dispatchers consume the event will the keyboard focus manager dispatch it to the focus owner in the normal way.

KeyEventPostProcessor

Enables clients to access the stream of KeyEvents after they have been handled by the focus owner. Each KeyEventPostProcessor is given a chance to examine the events (in the order in which they were added) and perform any desired post-processing. Each may also halt further post-processing of events.

28.1.4 The DefaultKeyboardFocusManager Class

The DefaultFocusManager class is a concrete implementation of the abstract KeyboardFocusManager class, performing the standard focus-related tasks for AWT and Swing applications. Because there are so many places you can plug in your own filters and behavior, you're far less likely to want to replace this class with your own than in previous Java releases. (In fact, readers who have access to the earlier edition of this book may want to compare the following example with the one starting on page 1125 in that edition, to see just how much simpler life has become! The new code is shorter, more focused, and more elegant, even though it has added support for recursively handling nested containers.)

The default keyboard focus manager handles all of the behavior described at the start of this section and any behavior expected of a Java program, including focus traversal keys, delivery of keystrokes to the focus owner, menu activation in response to keystrokes that were not otherwise consumed, and the like. By registering focus traversal policies and keystroke sets, property change listeners (often vetoable), and key event dispatchers and post-processors, you can achieve an unprecedented degree of control over focus behavior in an elegant, encapsulated way.

28.1.5 The FocusTraversalPolicy Class

The FocusTraversalPolicy class is abstract, providing a convenient framework for the implementation of arbitrary focus traversal policies. Five methods must be implemented:

public Component getFirstComponent(Container focusCycleRoot)

Return the first component in the traversal cycle for the specified container. This is the component given focus when traversal "wraps" past the end of the cycle.

public Component getLastComponent(Container focusCycleRoot)

Return the last component in the traversal cycle for the specified container.

public Component getDefaultComponent(Container focusCycleRoot)

Return the default component to give focus. This is the component that is given focus when it first traverses down into the focus cycle root. It may be different from the first component in the cycle.

public Component getComponentAfter(Container focusCycleRoot, Component aComponent)

Return the component that comes after the specified one in the traversal cycle. focusCycleRoot must be a focus cycle root of the component.

public Component getComponentBefore(Container focusCycleRoot, Component aComponent)

Return the component that comes before the specified one in the traversal cycle. focusCycleRoot must be a focus cycle root of the component.

There is also one method you can override; the default definition is usually fine:

public Component getInitialComponent(Container focusCycleRoot)

Return the component that should be given focus when the container is first made visible. The standard implementation calls getDefaultComponent( ).

A couple of focus traversal policies are provided. Swing applications with a standard L&F (or any other L&F that extends BasicLookAndFeel) use LayoutFocusTraversalPolicy as their default policy for all containers.

28.1.6 Writing Your Own Focus Traversal Policy

Here's a new focus traversal with distinctly different behavior than the default policy. Our focus manager moves the focus through a series of buttons in alphabetical order, according to the button labels. This is not a completely general example, as it won't transfer focus to or from components other than buttons, and you may think you'd never want a user interface like the example program that demonstrates it. Although you'd be completely right, notice that changing just a few details would give you something very much like an icon view of a directory, allowing you to tab through files in alphabetical order regardless of the icons' physical order.

// AlphaButtonPolicy.java // import java.awt.*; import java.util.*; import javax.swing.*; public class AlphaButtonPolicy extends FocusTraversalPolicy {     private SortedMap getSortedButtons(Container focusCycleRoot) {         if (focusCycleRoot == null) {             throw new IllegalArgumentException("focusCycleRoot can't be null");         }         SortedMap result = new TreeMap( );  // Will sort all buttons by text         sortRecursive(result, focusCycleRoot);         return result;     }     private void sortRecursive(Map buttons, Container container) {         for (int i = 0; i < container.getComponentCount( ); i++) {             Component c = container.getComponent(i);             if (c instanceof JButton) {  // Found another button to sort                 buttons.put(((JButton)c).getText( ), c);             }             if (c instanceof Container) {  // Found a container to search                 sortRecursive(buttons, (Container)c);             }         }     }     // The rest of the code implements the FocusTraversalPolicy interface.     public Component getFirstComponent(Container focusCycleRoot) {         SortedMap buttons = getSortedButtons(focusCycleRoot);         if (buttons.isEmpty( )) { return null; }         return (Component)buttons.get(buttons.firstKey( ));     }     public Component getLastComponent(Container focusCycleRoot) {         SortedMap buttons = getSortedButtons(focusCycleRoot);         if (buttons.isEmpty( )) { return null; }         return (Component)buttons.get(buttons.lastKey( ));     }          public Component getDefaultComponent(Container focusCycleRoot) {         return getFirstComponent(focusCycleRoot);     }          public Component getComponentAfter(Container focusCycleRoot,                                         Component aComponent) {         if (!(aComponent instanceof JButton)) { return null; }         SortedMap buttons = getSortedButtons(focusCycleRoot);         // Find all buttons after the current one.         String nextName = ((JButton)aComponent).getText( ) + "\0";         SortedMap nextButtons = buttons.tailMap(nextName);         if (nextButtons.isEmpty( )) {  // Wrapped back to beginning             if (!buttons.isEmpty( )) {                 return (Component)buttons.get(buttons.firstKey( ));             }             return null;  // Degenerate case of no buttons         }         return (Component)nextButtons.get(nextButtons.firstKey( ));     }          public Component getComponentBefore(Container focusCycleRoot,                                         Component aComponent) {         if (!(aComponent instanceof JButton)) { return null; }         SortedMap buttons = getSortedButtons(focusCycleRoot);         SortedMap prevButtons =  // Find all buttons before this one.             buttons.headMap(((JButton)aComponent).getText( ));         if (prevButtons.isEmpty( )) {  // Wrapped back to end             if (!buttons.isEmpty( )) {                 return (Component)buttons.get(buttons.lastKey( ));             }             return null;  // Degenerate case of no buttons         }         return (Component)prevButtons.get(prevButtons.lastKey( ));     } }

The first two methods in this class set up a SortedMap that contains all the buttons present in the container for which focus is being transferred, sorted by name. The first simply sets up context in which the second can perform a recursive descent through the container hierarchy, grabbing any buttons found. The rest of the class uses this map, and the wonderful power of the Java Collections framework, to implement the contract of the FocusTraversalPolicy with respect to alphabetically-sorted button labels.

If you plan to use such a policy with containers that held a large number of components, you may want to consider adding a cache to keep track of the sorted maps for each container the policy is asked to manage. This would save time during focus transfers, at the cost of complexity (the policy would have to listen for container events on each container it was managing so it would know when it needed to invalidate its cache entry).

Here is a simple program that demonstrates the new focus traversal policy:

//FocusTraversalExample.java // import java.awt.*; import java.awt.event.*; import javax.swing.*; public class FocusTraversalExample extends JPanel {     public FocusTraversalExample( ) {         setLayout(new GridLayout(6, 1));         JButton button1 = new JButton("Texas");         JButton button2 = new JButton("Vermont");         JButton button3 = new JButton("Florida");         JButton button4 = new JButton("Alabama");         JButton button5 = new JButton("Minnesota");         JButton button6 = new JButton("California");         setBackground(Color.lightGray);         add(button1);         add(button2);         add(button3);         add(button4);         add(button5);         add(button6);     }     public static void main(String[] args) {         JFrame frame = new JFrame("Alphabetized Button Focus Traversal");         frame.setFocusTraversalPolicy(new AlphaButtonPolicy( ));         frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);         frame.setContentPane(new FocusTraversalExample( ));         frame.setSize(400, 300);         frame.setVisible(true);     } }

The example is quite straightforward. The call to setFocusTraversalPolicy( ) installs our new focus manager. Figure 28-3 shows what the program looks like.

Figure 28-3. JButtons using the alphabetized button focus manager
figs/swng2.2803.gif


Java Swing
Graphic Java 2: Mastering the Jfc, By Geary, 3Rd Edition, Volume 2: Swing
ISBN: 0130796670
EAN: 2147483647
Year: 2001
Pages: 289
Authors: David Geary

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net