blob: 8c7fe08c62dfb5d4ee7eab64729c61172654839e [file] [log] [blame]
////////////////////////////////////////////////////////////////////////////////
//
// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package spark.components.supportClasses
{
import flash.display.DisplayObject;
import flash.display.DisplayObjectContainer;
import flash.events.Event;
import flash.events.EventDispatcher;
import flash.events.FocusEvent;
import flash.events.KeyboardEvent;
import flash.events.MouseEvent;
import flash.events.TimerEvent;
import flash.ui.Keyboard;
import flash.utils.Timer;
import mx.core.IUIComponent;
import mx.core.mx_internal;
import mx.events.FlexEvent;
import mx.events.SandboxMouseEvent;
import mx.managers.ISystemManager;
import spark.components.DropDownList;
import spark.events.DropDownEvent;
use namespace mx_internal;
/**
* The DropDownController class handles the mouse, keyboard, and focus
* interactions for an anchor button and its associated drop down.
* This class is used by the drop-down components, such as DropDownList,
* to handle the opening and closing of the drop down due to user interactions.
*
* @see spark.components.DropDownList
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class DropDownController extends EventDispatcher
{
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function DropDownController()
{
super();
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
private var mouseIsDown:Boolean;
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// openButton
//----------------------------------
private var _openButton:ButtonBase;
/**
* A reference to the <code>openButton</code> skin part
* of the drop-down component.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function set openButton(value:ButtonBase):void
{
if (_openButton === value)
return;
removeOpenTriggers();
_openButton = value;
if (_openButton)
_openButton.disableMinimumDownStateTime = true;
addOpenTriggers();
}
/**
* @private
*/
public function get openButton():ButtonBase
{
return _openButton;
}
/**
* @private
*/
private var _systemManager:ISystemManager;
/**
* A reference to the <code>SystemManager</code> used
* for mouse tracking. if none is specified, the controller
* will use the systemManager associated with the openButton.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function set systemManager(value:ISystemManager):void
{
_systemManager = value;
}
/**
* @private
*/
public function get systemManager():ISystemManager
{
return (_systemManager != null)? _systemManager:
(openButton != null)? openButton.systemManager:
null;
}
/**
* A list of display objects to consider part of the hit area
* of the drop down. Mouse clicks within any component listed
* as an inclusion will not automatically close the drop down.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public var hitAreaAdditions:Vector.<DisplayObject>;
//----------------------------------
// dropDown
//----------------------------------
private var _dropDown:DisplayObject;
/**
* @private
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function set dropDown(value:DisplayObject):void
{
if (_dropDown === value)
return;
_dropDown = value;
}
/**
* @private
*/
public function get dropDown():DisplayObject
{
return _dropDown;
}
//----------------------------------
// isOpen
//----------------------------------
/**
* @private
*/
private var _isOpen:Boolean = false;
/**
* Contains <code>true</code> if the drop down is open.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get isOpen():Boolean
{
return _isOpen;
}
//----------------------------------
// closeOnResize
//----------------------------------
private var _closeOnResize:Boolean = true;
/**
* When <code>true</code>, resizing the system manager
* closes the drop down.
* For mobile applications, you can set this property
* to <code>false</code> so that the drop down stays open when the
* page orientation changes.
*
* @default true
*
* @langversion 3.0
* @playerversion AIR 3
* @productversion Flex 4.6
*/
public function get closeOnResize():Boolean
{
return _closeOnResize;
}
/**
* @private
*/
public function set closeOnResize(value:Boolean):void
{
if (_closeOnResize == value)
return;
// remove existing resize listener if present
if (isOpen)
removeCloseOnResizeTrigger();
_closeOnResize = value;
addCloseOnResizeTrigger();
}
//----------------------------------
// rolloverOpenDelay
//----------------------------------
private var _rollOverOpenDelay:Number = Number.NaN;
private var rollOverOpenDelayTimer:Timer;
/**
* Specifies the delay, in milliseconds, to wait for opening the drop down
* when the anchor button is rolled over.
* If set to <code>NaN</code>, then the drop down opens on a click, not a rollover.
*
* @default NaN
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function get rollOverOpenDelay():Number
{
return _rollOverOpenDelay;
}
/**
* @private
*/
public function set rollOverOpenDelay(value:Number):void
{
if (_rollOverOpenDelay == value)
return;
removeOpenTriggers();
_rollOverOpenDelay = value;
addOpenTriggers();
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
* Adds event triggers to the openButton to open the popup.
*
* <p>This is called from the openButton setter after the openButton has been set.</p>
*/
private function addOpenTriggers():void
{
if (openButton)
{
if (isNaN(rollOverOpenDelay))
openButton.addEventListener(FlexEvent.BUTTON_DOWN, openButton_buttonDownHandler);
else
openButton.addEventListener(MouseEvent.ROLL_OVER, openButton_rollOverHandler);
}
}
/**
* @private
* Removes event triggers from the openButton to open the popup.
*
* <p>This is called from the openButton setter after the openButton has been set.</p>
*/
private function removeOpenTriggers():void
{
// TODO (jszeto): Change this to be mouseDown. Figure out how to not
// trigger systemManager_mouseDown.
if (openButton)
{
if (isNaN(rollOverOpenDelay))
openButton.removeEventListener(FlexEvent.BUTTON_DOWN, openButton_buttonDownHandler);
else
openButton.removeEventListener(MouseEvent.ROLL_OVER, openButton_rollOverHandler);
}
}
/**
* @private
* Adds event triggers close the popup.
*
* <p>This is called when the drop down is popped up.</p>
*/
private function addCloseTriggers():void
{
if (systemManager)
{
if (isNaN(rollOverOpenDelay))
{
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_DOWN, systemManager_mouseDownHandler);
systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_DOWN_SOMEWHERE, systemManager_mouseDownHandler);
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_UP, systemManager_mouseUpHandler_noRollOverOpenDelay);
}
else
{
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_MOVE, systemManager_mouseMoveHandler);
systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_MOVE_SOMEWHERE, systemManager_mouseMoveHandler);
// MOUSEUP triggers may be added in systemManager_mouseMoveHandler
}
addCloseOnResizeTrigger();
if (openButton && openButton.systemManager)
openButton.systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_WHEEL, systemManager_mouseWheelHandler);
}
}
private function addCloseOnResizeTrigger():void
{
if (closeOnResize)
systemManager.getSandboxRoot().addEventListener(Event.RESIZE, systemManager_resizeHandler, false, 0, true);
}
/**
* @private
* Adds event triggers close the popup.
*
* <p>This is called when the drop down is closed.</p>
*/
private function removeCloseTriggers():void
{
if (systemManager)
{
if (isNaN(rollOverOpenDelay))
{
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_DOWN, systemManager_mouseDownHandler);
systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_DOWN_SOMEWHERE, systemManager_mouseDownHandler);
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_UP, systemManager_mouseUpHandler_noRollOverOpenDelay);
}
else
{
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_MOVE, systemManager_mouseMoveHandler);
systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_MOVE_SOMEWHERE, systemManager_mouseMoveHandler);
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_UP, systemManager_mouseUpHandler);
systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, systemManager_mouseUpHandler);
}
removeCloseOnResizeTrigger();
if (openButton && openButton.systemManager)
openButton.systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_WHEEL, systemManager_mouseWheelHandler);
}
}
private function removeCloseOnResizeTrigger():void
{
if (closeOnResize)
systemManager.getSandboxRoot().removeEventListener(Event.RESIZE, systemManager_resizeHandler);
}
/**
* @private
* Helper method for the mouseMove and mouseUp handlers to see if
* the mouse is over a "valid" region. This is used to help determine
* when the dropdown should be closed.
*/
private function isTargetOverDropDownOrOpenButton(target:DisplayObject):Boolean
{
if (target)
{
// check if the target is the openButton or contained within the openButton
if (openButton && openButton.contains(target))
return true;
if (hitAreaAdditions != null)
{
for (var i:int = 0;i<hitAreaAdditions.length;i++)
{
if (hitAreaAdditions[i] == target ||
((hitAreaAdditions[i] is DisplayObjectContainer) && DisplayObjectContainer(hitAreaAdditions[i]).contains(target as DisplayObject)))
return true;
}
}
// check if the target is the dropdown or contained within the dropdown
if (dropDown is DisplayObjectContainer)
{
if (DisplayObjectContainer(dropDown).contains(target))
return true;
}
else
{
if (target == dropDown)
return true;
}
}
return false;
}
/**
* Open the drop down and dispatch a <code>DropdownEvent.OPEN</code> event.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function openDropDown():void
{
openDropDownHelper(true);
}
/**
* @private
* Set isProgrammatic to true if you are opening the dropDown programmatically
* or not through a mouse click or rollover.
*/
private function openDropDownHelper(isProgrammatic:Boolean = false):void
{
if (!isOpen)
{
addCloseTriggers();
_isOpen = true;
// Force the button to stay in the down state
if (openButton)
openButton.keepDown(true, !isProgrammatic);
dispatchEvent(new DropDownEvent(DropDownEvent.OPEN));
}
}
/**
* Close the drop down and dispatch a <code>DropDownEvent.CLOSE</code> event.
*
* @param commit If <code>true</code>, commit the selected
* data item.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function closeDropDown(commit:Boolean):void
{
if (isOpen)
{
_isOpen = false;
if (openButton)
openButton.keepDown(false);
var dde:DropDownEvent = new DropDownEvent(DropDownEvent.CLOSE, false, true);
if (!commit)
dde.preventDefault();
dispatchEvent(dde);
removeCloseTriggers();
}
}
//--------------------------------------------------------------------------
//
// Event handling
//
//--------------------------------------------------------------------------
/**
* @private
* Called when the buttonDown event is dispatched. This function opens or closes
* the dropDown depending upon the dropDown state.
*/
mx_internal function openButton_buttonDownHandler(event:Event):void
{
if (isOpen)
closeDropDown(true);
else
{
mouseIsDown = true;
openDropDownHelper();
}
}
/**
* @private
* Called when the openButton's <code>rollOver</code> event is dispatched. This function opens
* the drop down, or opens the drop down after the length of time specified by the
* <code>rollOverOpenDelay</code> property.
*/
mx_internal function openButton_rollOverHandler(event:MouseEvent):void
{
if (rollOverOpenDelay == 0)
openDropDownHelper();
else
{
openButton.addEventListener(MouseEvent.ROLL_OUT, openButton_rollOutHandler);
rollOverOpenDelayTimer = new Timer(rollOverOpenDelay, 1);
rollOverOpenDelayTimer.addEventListener(TimerEvent.TIMER_COMPLETE, rollOverDelay_timerCompleteHandler);
rollOverOpenDelayTimer.start();
}
}
/**
* @private
* Called when the openButton's rollOut event is dispatched while waiting
* for the rollOverOpenDelay. This will cancel the timer so we don't open
* any more.
*/
private function openButton_rollOutHandler(event:MouseEvent):void
{
if (rollOverOpenDelayTimer && rollOverOpenDelayTimer.running)
{
rollOverOpenDelayTimer.stop();
rollOverOpenDelayTimer = null;
}
openButton.removeEventListener(MouseEvent.ROLL_OUT, openButton_rollOutHandler);
}
/**
* @private
* Called when the rollOverDelay Timer is up and we should show the drop down.
*/
private function rollOverDelay_timerCompleteHandler(event:TimerEvent):void
{
openButton.removeEventListener(MouseEvent.ROLL_OUT, openButton_rollOutHandler);
rollOverOpenDelayTimer = null;
openDropDownHelper();
}
/**
* @private
* Called when the systemManager receives a mouseDown event. This closes
* the dropDown if the target is outside of the dropDown.
*/
mx_internal function systemManager_mouseDownHandler(event:Event):void
{
// stop here if mouse was down from being down on the open button
if (mouseIsDown)
{
mouseIsDown = false;
return;
}
if (!dropDown ||
(dropDown &&
(event.target == dropDown
|| (dropDown is DisplayObjectContainer &&
!DisplayObjectContainer(dropDown).contains(DisplayObject(event.target))))))
{
// don't close if it's on the openButton
var target:DisplayObject = event.target as DisplayObject;
if (openButton && target && openButton.contains(target))
return;
if (hitAreaAdditions != null)
{
var length:int = hitAreaAdditions.length;
for (var i:int = 0;i < length; i++)
{
if (hitAreaAdditions[i] == target ||
((hitAreaAdditions[i] is DisplayObjectContainer) && DisplayObjectContainer(hitAreaAdditions[i]).contains(target)))
return;
}
}
// contains() doesn't cover popups/dropdowns, but owns() does.
if (dropDown is IUIComponent)
{
if ((dropDown as IUIComponent).owns(target))
return;
}
closeDropDown(true);
}
}
/**
* @private
* Called when the dropdown is popped up from a rollover and the mouse moves
* anywhere on the screen. If the mouse moves over the openButton or the dropdown,
* the popup will stay open. Otherwise, the popup will close.
*/
mx_internal function systemManager_mouseMoveHandler(event:Event):void
{
var target:DisplayObject = event.target as DisplayObject;
var containedTarget:Boolean = isTargetOverDropDownOrOpenButton(target);
if (containedTarget)
return;
// if the mouse is down, wait until it's released to close the drop down
if ((event is MouseEvent && MouseEvent(event).buttonDown) ||
(event is SandboxMouseEvent && SandboxMouseEvent(event).buttonDown))
{
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_UP, systemManager_mouseUpHandler);
systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, systemManager_mouseUpHandler);
return;
}
closeDropDown(true);
}
/**
* @private
* Debounce the mouse
*/
mx_internal function systemManager_mouseUpHandler_noRollOverOpenDelay(event:Event):void
{
// stop here if mouse was down from being down on the open button
if (mouseIsDown)
{
mouseIsDown = false;
return;
}
}
/**
* @private
* Called when the dropdown is popped up from a rollover and the mouse is released
* anywhere on the screen. This will close the popup.
*/
mx_internal function systemManager_mouseUpHandler(event:Event):void
{
var target:DisplayObject = event.target as DisplayObject;
var containedTarget:Boolean = isTargetOverDropDownOrOpenButton(target);
// if we're back over the target area, remove this event listener
// and do nothing. we handle this in mouseMoveHandler()
if (containedTarget)
{
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_UP, systemManager_mouseUpHandler);
systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE, systemManager_mouseUpHandler);
return;
}
closeDropDown(true);
}
/**
* @private
* Close the dropDown if the stage has been resized.
*/
mx_internal function systemManager_resizeHandler(event:Event):void
{
closeDropDown(true);
}
/**
* @private
* Called when the mouseWheel is used
*/
private function systemManager_mouseWheelHandler(event:MouseEvent):void
{
// Close the dropDown unless we scrolled over the dropdown and the dropdown handled the event
if (dropDown && !(DisplayObjectContainer(dropDown).contains(DisplayObject(event.target)) && event.isDefaultPrevented()))
closeDropDown(false);
}
/**
* Close the drop down if it is no longer in focus.
*
* @param event The event object for the <code>FOCUS_OUT</code> event.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function processFocusOut(event:FocusEvent):void
{
// Note: event.relatedObject is the object getting focus.
// It can be null in some cases, such as when you open
// the dropdown and then click outside the application.
// If the dropdown is open...
if (isOpen)
{
// If focus is moving outside the dropdown...
if (!event.relatedObject ||
(!dropDown ||
(dropDown is DisplayObjectContainer &&
!DisplayObjectContainer(dropDown).contains(event.relatedObject))))
{
// Close the dropdown.
closeDropDown(true);
}
}
}
/**
* Handles the keyboard user interactions.
*
* @param event The event object from the keyboard event.
*
* @return Returns <code>true</code> if the <code>keyCode</code> was
* recognized and handled.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function processKeyDown(event:KeyboardEvent):Boolean
{
if (event.isDefaultPrevented())
return true;
if (event.ctrlKey && event.keyCode == Keyboard.DOWN)
{
openDropDownHelper(true); // Programmatically open
event.preventDefault();
}
else if (event.ctrlKey && event.keyCode == Keyboard.UP)
{
closeDropDown(true);
event.preventDefault();
}
else if (event.keyCode == Keyboard.ENTER)
{
// Close the dropDown and eat the event if appropriate.
if (isOpen)
{
closeDropDown(true);
event.preventDefault();
}
}
else if (event.keyCode == Keyboard.ESCAPE)
{
// Close the dropDown and eat the event if appropriate.
if (isOpen)
{
closeDropDown(false);
event.preventDefault();
}
}
else
{
return false;
}
return true;
}
}
}