blob: d1ae7d72f70c8de88c5b12446053922295baa8c8 [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.events.Event;
import flash.events.EventDispatcher;
import flash.events.MouseEvent;
import flash.events.TimerEvent;
import flash.utils.Timer;
import mx.core.InteractionMode;
import mx.core.UIComponent;
import mx.core.mx_internal;
import mx.events.SandboxMouseEvent;
import mx.events.TouchInteractionEvent;
import mx.managers.ISystemManager;
use namespace mx_internal;
/**
* Dispatched after the state has changed.
*
* @eventType flash.events.Event.CHANGE
*
* @langversion 3.0
* @playerversion Flash 10.1
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
[Event(name="change", type="flash.events.Event")]
/**
* A helper class for components to use to help them determine
* if they should be in the up, over, or down states.
*
* <p>As the state changes, if the transition should play, the
* playTransitions.</p>
*
* @see spark.components.supportClasses.InteractionState
*
* @langversion 3.0
* @playerversion Flash 10.1
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public class InteractionStateDetector extends EventDispatcher
{
/**
* Constructor
*
* @param components The UIComponent to detect the up/over/down state on.
* The event listeners are attached to this object.
*
* @langversion 3.0
* @playerversion Flash 10.1
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function InteractionStateDetector(component:UIComponent)
{
super();
this.component = component;
addHandlers();
}
//--------------------------------------------------------------------------
//
// Private Properties
//
//--------------------------------------------------------------------------
/**
* @private
* The UIComponent to detect the hovered and down state for
*/
private var component:UIComponent;
/**
* @private
* Timer for putting the renderer in the down state on a delay
* timer because of touch input.
*/
private var mouseDownSelectTimer:Timer;
/**
* @private
* Timer for putting the renderer in the up state. This makes sure
* even when we have a delay to select an item and someone mouses up
* before that delay, the user still gets some visual feedback that
* the renderer was actually selected.
*/
private var mouseUpDeselectTimer:Timer;
/**
* @private
* When faking a mouseDown after a mouse up has occurred, if we get a rollOut
* event, we don't want to immediately set hovered = false so we can maintain
* the down state until the mouseUpDeselectTimer is finished. So we keep track
* that a rollOut event occurred and honor it later.
*/
private var rollOutWhileFakingDownState:Boolean = false;
/**
* @private
* Whether the component using this InteractionStateDetector should
* play transitions on a particular state change.
*
* <p>This could be moved to the CHANGE event itself, but
* seeing as we don't have a formal mechanism for dealing with ItemRenderer
* transitions in the first place, this seems like an acceptable solution.<p>
*
* <p>Currently, InteractionStateDetector is the one who would know whether a
* transition should play or not because it knows how it got in to a particular
* state and that's what we use to determine whether transitions play or not.
* For instance, if a scroll starts while you're in the down state, that should
* cancel the down state and not play a transition.</p>
*/
mx_internal var playTransitions:Boolean = true;
/**
* @private
* Keeps track of whether the system mouse handlers are installed
*/
private var systemMouseHandlersAdded:Boolean = false;
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
//----------------------------------
// hovered
//----------------------------------
/**
* @private
* Storage for the hovered property
*/
private var _hovered:Boolean = false;
/**
* @private
*/
private function get hovered():Boolean
{
return _hovered;
}
/**
* @private
*/
private function set hovered(value:Boolean):void
{
if (value == _hovered)
return;
_hovered = value;
invalidateState();
}
//----------------------------------
// mouseCaptured
//----------------------------------
/**
* @private
* Storage for the mouseCaptured property
*/
private var _mouseCaptured:Boolean = false;
/**
* @private
* Indicates whether the mouse is down and the mouse pointer was
* over the renderer when <code>MouseEvent.MOUSE_DOWN</code> was first dispatched.
* Used to determine the skin state.
*/
private function get mouseCaptured():Boolean
{
return _mouseCaptured;
}
/**
* @private
*/
private function set mouseCaptured(value:Boolean):void
{
// System mouse handlers are not needed when the renderer is not mouse captured
// NOTE: do this before the short-circuit because setting false needs to remove
// the handlers even if the value is already false.
if (!value)
removeSystemMouseHandlers();
if (value == _mouseCaptured)
return;
_mouseCaptured = value;
invalidateState();
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// state
//----------------------------------
[Bindable("change")]
/**
* Returns the state of the component
*
* <p>Possible values are:
* <ul>
* <li>InteractionState.UP</li>
* <li>InteractionState.DOWN</li>
* <li>InteractionState.OVER</li>
* </ul>
* </p>
*
* @see spark.components.supportClasses.InteractionState;
*
* @langversion 3.0
* @playerversion Flash 10.1
* @playerversion AIR 2.5
* @productversion Flex 4.5
*/
public function get state():String
{
if (isDown())
return InteractionState.DOWN;
else if (hovered && component.getStyle("interactionMode") == InteractionMode.MOUSE)
return InteractionState.OVER;
else
return InteractionState.UP;
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* @private
* Helper method to determine if someone has down down on
* the display object.
*/
private function isDown():Boolean
{
return (mouseCaptured && _hovered);
}
/**
* @private
* Called when the state becomes invalid. This just
* in turn dispatches a CHANGE event.
*/
private function invalidateState():void
{
dispatchEvent(new Event(Event.CHANGE));
}
/**
* @private
*/
private function addHandlers():void
{
component.addEventListener(MouseEvent.ROLL_OVER, mouseEventHandler);
component.addEventListener(MouseEvent.ROLL_OUT, mouseEventHandler);
component.addEventListener(MouseEvent.MOUSE_DOWN, mouseEventHandler);
component.addEventListener(MouseEvent.MOUSE_UP, mouseEventHandler);
component.addEventListener(MouseEvent.CLICK, mouseEventHandler);
component.addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_START, touchInteractionStartHandler);
}
/**
* @private
* This method adds the systemManager_mouseUpHandler as an event listener to
* the stage and the systemManager so that it gets called even if mouse events
* are dispatched outside of the renderer. This is needed for example when the
* user presses the renderer, drags out and releases the renderer.
*/
private function addSystemMouseHandlers():void
{
var systemManager:ISystemManager = component.systemManager;
if (systemManager && !systemMouseHandlersAdded)
{
systemManager.getSandboxRoot().addEventListener(
MouseEvent.MOUSE_UP, systemManager_mouseUpHandler, true /* useCapture */);
systemManager.getSandboxRoot().addEventListener(
SandboxMouseEvent.MOUSE_UP_SOMEWHERE, systemManager_mouseUpHandler);
systemMouseHandlersAdded = true;
}
}
/**
* @private
* This method removes the systemManager_mouseUpHandler as an event
* listener from the stage and the systemManager.
*/
private function removeSystemMouseHandlers():void
{
var systemManager:ISystemManager = component.systemManager;
if (systemManager && systemMouseHandlersAdded)
{
systemManager.getSandboxRoot().removeEventListener(
MouseEvent.MOUSE_UP, systemManager_mouseUpHandler, true /* useCapture */);
systemManager.getSandboxRoot().removeEventListener(
SandboxMouseEvent.MOUSE_UP_SOMEWHERE, systemManager_mouseUpHandler);
systemMouseHandlersAdded = false;
}
}
/**
* @private
* Starts timer to select the renderer
*/
private function startSelectRendererAfterDelayTimer():void
{
var touchDelay:Number = component.getStyle("touchDelay");
if (touchDelay > 0)
{
mouseDownSelectTimer = new Timer(touchDelay, 1);
mouseDownSelectTimer.addEventListener(TimerEvent.TIMER_COMPLETE, mouseDownSelectTimer_timerCompleteHandler);
mouseDownSelectTimer.start();
}
else
{
mouseDownSelectTimer_timerCompleteHandler();
}
}
/**
* @private
*/
private function stopSelectRendererAfterDelayTimer():void
{
if (mouseDownSelectTimer)
{
mouseDownSelectTimer.stop();
mouseDownSelectTimer = null;
}
}
/**
* @private
* Starts timer to deselect the renderer if the mouseup happened too quickly
* after the mousedown so that no mousedown state was entered in to.
*/
private function startDeselectRendererAfterDelayTimer():void
{
var touchDelay:Number = component.getStyle("touchDelay");
if (touchDelay > 0)
{
mouseUpDeselectTimer = new Timer(touchDelay, 1);
mouseUpDeselectTimer.addEventListener(TimerEvent.TIMER_COMPLETE, mouseUpDeselectTimer_timerCompleteHandler);
mouseUpDeselectTimer.start();
}
else
{
mouseUpDeselectTimer_timerCompleteHandler();
}
}
/**
* @private
*/
private function stopDeselectRendererAfterDelayTimer():void
{
if (mouseUpDeselectTimer)
{
mouseUpDeselectTimer.stop();
mouseUpDeselectTimer = null;
}
}
//--------------------------------------------------------------------------
//
// Event Handlers
//
//--------------------------------------------------------------------------
/**
* @private
* This method handles the mouse events, calls the <code>clickHandler</code> method
* where appropriate and updates the <code>hovered</code> and
* <code>mouseCaptured</code> properties.
*
* <p>This method gets called to handle <code>MouseEvent.ROLL_OVER</code>,
* <code>MouseEvent.ROLL_OUT</code>, <code>MouseEvent.MOUSE_DOWN</code>,
* <code>MouseEvent.MOUSE_UP</code>, and <code>MouseEvent.CLICK</code> events.</p>
*
* @param event The Event object associated with the event.
*/
private function mouseEventHandler(event:Event):void
{
var mouseEvent:MouseEvent = event as MouseEvent;
switch (event.type)
{
case MouseEvent.ROLL_OVER:
{
// if the user rolls over while holding the mouse button
if (mouseEvent.buttonDown && !mouseCaptured)
return;
hovered = true;
rollOutWhileFakingDownState = false;
break;
}
case MouseEvent.ROLL_OUT:
{
if (mouseUpDeselectTimer && mouseUpDeselectTimer.running)
{
// We're trying to flash the down state for longer,
// so let's not leave the hovered state just yet
rollOutWhileFakingDownState = true;
}
else
{
hovered = false;
}
break;
}
case MouseEvent.MOUSE_DOWN:
{
// since mouseDowns are cancellable, let's check to see
// if anyone's handled it already
if (event.isDefaultPrevented())
break;
// if we were going to unhighlight ourselves, don't do it as we
// are just going to highlight again
stopDeselectRendererAfterDelayTimer();
// When the button is down we need to listen for mouse events outside the renderer so that
// we update the state appropriately on mouse up. Whenever mouseCaptured changes to false,
// it will take care to remove those handlers.
addSystemMouseHandlers();
// if we're in touchMode, let's delay our selection until later
// otherwise, when touch scrolling, the renderer might flicker
if (component.getStyle("interactionMode") == InteractionMode.TOUCH)
{
startSelectRendererAfterDelayTimer();
}
else
{
mouseCaptured = true;
}
// we don't call event.preventDefault() here since List.item_mouseDownHandler will call this anyways
break;
}
case MouseEvent.MOUSE_UP:
{
// If someone mouses up on us, then they must be hovered over
// us now.
hovered = true;
if (mouseDownSelectTimer && mouseDownSelectTimer.running)
{
// We never even flashed the down state for this click operation.
// There are two possibilities for being here:
// 1) mouseCaptured wasn't set to true (meaning this is the first click)
// 2) mouseCaptured was true (meaning a click operation hadn't finished
// and we find ourselves in here again--perhaps it was a doublet tap).
// In either case, let's make sure that down state shows up for a little bit
// before going back to the up state.
// stop the original timer, put it in mouse down state, then start a new
// timer to undo the mouse down state
stopSelectRendererAfterDelayTimer();
mouseCaptured = true;
startDeselectRendererAfterDelayTimer();
}
else if (mouseCaptured)
{
mouseCaptured = false;
}
break;
}
case MouseEvent.CLICK:
{
return;
}
}
}
/**
* @private
*/
private function systemManager_mouseUpHandler(event:Event):void
{
// If the target is the renderer, do nothing because the
// mouseEventHandler will be handle it.
if (event.target == component || component.contains(event.target as DisplayObject))
{
return;
}
// If faking down state, let's not interrupt it because of a mouseUp somewhere
// else on the screen
if (!(mouseUpDeselectTimer && mouseUpDeselectTimer.running))
mouseCaptured = false;
// If the mouseDownSelectTimer is still running,
// we don't want to ever go in to the down state in this case, so stop it
if (mouseDownSelectTimer && mouseDownSelectTimer.running)
stopSelectRendererAfterDelayTimer();
}
/**
* @private
*/
private function touchInteractionStartHandler(event:TouchInteractionEvent):void
{
// if we have a timer going on, let's stop it to make sure we don't
// select the renderer later
stopSelectRendererAfterDelayTimer();
// cancel the rollover/clickdown on and go back to a normal state
// turn off transitions for this change because it's really cancelling the
// the down state
playTransitions = false;
hovered = false;
mouseCaptured = false;
playTransitions = true;
}
/**
* @private
*/
private function mouseDownSelectTimer_timerCompleteHandler(event:TimerEvent = null):void
{
mouseCaptured = true;
}
/**
* @private
*/
private function mouseUpDeselectTimer_timerCompleteHandler(event:TimerEvent = null):void
{
mouseCaptured = false;
// if we got a rollout, we should honor it now
if (rollOutWhileFakingDownState)
{
rollOutWhileFakingDownState = false;
hovered = false;
}
}
/**
* @private
*/
private function anyButtonDown(event:MouseEvent):Boolean
{
var type:String = event.type;
// TODO (rfrishbe): we should not code to literals here (and other places where this code is used)
return event.buttonDown || (type == "middleMouseDown") || (type == "rightMouseDown");
}
}
}