| //////////////////////////////////////////////////////////////////////////////// |
| // |
| // 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"); |
| } |
| |
| |
| } |
| } |