While working with asynchronous operations like server calls or asynchronous events, you know that it’s not easy to guarantee a correct processing of user’s requests if there isn’t a well defined workflow that the user must follow. For example: the user clicks a button that makes a function call on the server side, so the application waits for the server to reply, but meanwhile the application is still responsive for the user so any other operation could be performed by the user on the interface possibly not compatible with the reply of the server in the moment that the client receives it. There are times when the user interaction must be disabled until the application is again in a state that can safely process any other user request because there’s a well defined workflow to follow. This is the topic of this post, giving you a way to temporarily disable user interaction while the application is not in a ready state.
At this point one could think: well, that’s easy, you can do that simply by setting the enabled property of the components. No, it’s not that easy. The reason why it’s not easy, is because you don’t want the user to see weird color changes in the user interface because a component switches from enabled to disabled state and vice versa and you want to keep the focus on the component that was focused before disabling the interface. So these are the key points that I had to keep in mind while thinking about the solution that I’m going to show you.
As usual, the best way to start is looking at the source code, so we can discuss later about what it does.
package com.devahead.utils
{
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.events.EventDispatcher;
import mx.collections.ArrayCollection;
import mx.containers.Canvas;
import mx.core.Application;
import mx.core.IChildList;
import mx.core.UIComponent;
public class UserInteractionManager extends EventDispatcher
{
protected var _blocker: Canvas = null;
protected var _managedApp: Application = null;
/**
* Application managed by this UserInteractionManager instance.
*/
public function get managedApp(): Application
{
return _managedApp;
}
protected var _focusState: Object = null;
/**
* Data about the focused components before disabling the
* user interaction.
*/
protected function get focusState(): Object
{
return _focusState;
}
protected function set focusState(value: Object): void
{
_focusState = value;
dispatchEvent(new Event("isInteractionEnabledChanged"));
}
/**
* Tells if the user interaction is currently enabled.
*/
[Bindable("isInteractionEnabledChanged")]
public function get isInteractionEnabled(): Boolean
{
return (_focusState == null);
}
/**
* Constructor.
*
* @param managedApp
* Application instance managed by UserInteractionManager.
*/
public function UserInteractionManager(managedApp: Application)
{
_managedApp = managedApp;
// Create the blocker component to avoid mouse clicks.
// We set a backgroundColor to actually allow the clicks to be blocked
// (it doesn't work if the blocker is totally transparent) and we set
// the backgroundAlpha to a very small number so the blocker is not
// transparent for the Flash player but it is actually invisible for the
// human eye.
_blocker = new Canvas();
_blocker.setStyle("backgroundColor", 0xffffff);
_blocker.setStyle("backgroundAlpha", 0.01);
}
/**
* Disable the user interaction with the application.
*/
public function disableUserInteraction(): void
{
// Check if interaction is already disabled
if (!isInteractionEnabled) return;
// Save the information about the current focused component
focusState = new Object();
if (_managedApp.focusManager != null)
{
focusState.focusedComponent = _managedApp.focusManager.getFocus();
if (focusState.focusedComponent != null &&
focusState.focusedComponent.hasOwnProperty("selectionBeginIndex") &&
focusState.focusedComponent.hasOwnProperty("selectionEndIndex"))
{
// Save the selection information about the focused component (useful for text
// selection inside a TextInput for example).
focusState.focusedCompSelectionBeginIndex = focusState.focusedComponent.selectionBeginIndex;
focusState.focusedCompSelectionEndIndex = focusState.focusedComponent.selectionEndIndex;
}
}
// Set the size of the blocker
_blocker.width = _managedApp.width;
_blocker.height = _managedApp.height;
// Make the blocker the topmost component in the display list
var rawChildren: IChildList = _managedApp.systemManager.rawChildren;
if (rawChildren.contains(_blocker))
{
rawChildren.removeChild(_blocker);
}
rawChildren.addChild(_blocker);
if (_managedApp.stage != null)
{
// Remove the focus from the current focused component
_managedApp.stage.focus = null;
}
if (_managedApp.systemManager is DisplayObjectContainer)
{
// Disable tabbing so the user cannot use the tab key to cycle
// among the components.
focusState.sysManTabChildren = (_managedApp.systemManager as DisplayObjectContainer).tabChildren;
(_managedApp.systemManager as DisplayObjectContainer).tabChildren = false;
}
}
/**
* Restore the user interaction with the application.
*/
public function restoreUserInteraction(): void
{
// Check if the interaction is already enabled
if (isInteractionEnabled) return;
if (_managedApp.systemManager is DisplayObjectContainer)
{
// Restore the tabbing state saved before disabling the user interaction
(_managedApp.systemManager as DisplayObjectContainer).tabChildren = focusState.sysManTabChildren;
}
// Remove the blocker from the display list
var rawChildren: IChildList = _managedApp.systemManager.rawChildren;
if (rawChildren.contains(_blocker))
{
rawChildren.removeChild(_blocker);
}
if (focusState.hasOwnProperty("focusedComponent") &&
focusState.focusedComponent != null &&
_managedApp.focusManager != null)
{
// Check if I can restore the focus or not depending on whether or not there
// are modal popups over the previously focused component.
var canRestoreFocus: Boolean = false;
if (_managedApp.systemManager.numModalWindows > 0)
{
// Get the list of all the active popups
var popupList: ArrayCollection = PopUpUtils.getAllPopups(_managedApp, true);
if (popupList.length == 0)
{
// There are no popups, so I can restore the focus
canRestoreFocus = true;
}
else
{
if (focusState.focusedComponent is UIComponent)
{
// Get the topmost popup
var topmostPopup: Object = popupList.getItemAt(popupList.length - 1);
var currParent: Object = (focusState.focusedComponent as UIComponent).parent;
// Go through all the parents hierarchy for the component that should be
// focused to check if it belongs to the topmost popup.
while (currParent != null)
{
if (currParent == topmostPopup)
{
// The component that should be focused belongs to the topmost popup,
// so I can restore the focus.
canRestoreFocus = true;
break;
}
if (currParent == currParent.parent)
{
break;
}
currParent = currParent.parent;
}
}
else
{
canRestoreFocus = true;
}
}
}
else
{
// There are no modal popups, so I can always restore the focus
canRestoreFocus = true;
}
if (canRestoreFocus)
{
// Restore the focus on the component that was focused before disabling the
// user interaction.
_managedApp.focusManager.setFocus(focusState.focusedComponent);
if (focusState.focusedComponent.hasOwnProperty("selectionBeginIndex") &&
focusState.focusedComponent.hasOwnProperty("selectionEndIndex"))
{
// Restore the selection inside the focused component (useful for TextInput
// for example).
focusState.focusedComponent.selectionBeginIndex = focusState.focusedCompSelectionBeginIndex;
focusState.focusedComponent.selectionEndIndex = focusState.focusedCompSelectionEndIndex;
}
}
}
focusState = null;
}
}
}
This is the UserInteractionManager class and it manages all the necessary operations for you to disable and restore the user interaction. The constructor accepts the application that has to be managed by an instance of the class, so you’ll have a single instance of UserInteractionManager for an application. There are two functions in the class, disableUserInteraction and restoreUserInteraction.
When you decide to disable the interaction, UserInteractionManager saves the information about the current focused component so the focus can be restored later, then it puts a blocker canvas on top of every other child object in the application. Here we must use the rawChildren because the blocker must be also on top of the popups if present. After saving state information about the focused component and displaying the blocker, the focus is removed from the focused component and the tabChildren property of the systemManager is set to false so we ensure that the user cannot try to focus the components with the tab key. The blocker is not visible to the human eye because we set a very low backgroundAlpha value, but we must fill it with a color to make sure that the Flash player intercepts the mouse clicks on the blocker canvas instead of letting them go through the underlying components.
If you decide to restore the user interaction, then UserInteractionManager restores the tabChildren property of the systemManager, removes the blocker canvas and restores the focus on the previously focused component. While restoring the focus, we must also check if there are modal popups over the previously focused component. If the component is not a child of a topmost modal popup then it makes no sense to restore the focus on a component that is not supposed to have it since there’s a modal popup in front of it. To get all the popups in the application we use an utility class named PopUpUtils that we’ve already seen in a previous post. The properties selectionBeginIndex and selectionEndIndex are used with a TextInput or similar components to restore the text selection inside the focused component if some text was selected before disabling the user interaction.
Let’s take a look at an example application that uses the UserInteractionManager class. Read the rest of this entry »

Recent Comments