blob: 04cbd0546f4ac94d7fe8b3258c39f9856c1bb81f [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.FocusEvent;
import flash.events.MouseEvent;
import flash.geom.Point;
import mx.core.InteractionMode;
import mx.events.FlexEvent;
import mx.events.ResizeEvent;
import mx.events.SandboxMouseEvent;
import mx.events.TouchInteractionEvent;
import spark.components.Button;
import spark.events.TrackBaseEvent;
/**
* Dispatched when the value of the control changes
* as a result of user interaction.
*
* @eventType flash.events.Event.CHANGE
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="change", type="flash.events.Event")]
/**
* Dispatched at the end of a user interaction
* or when an animation ends.
*
* @eventType mx.events.FlexEvent.CHANGE_END
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="changeEnd", type="mx.events.FlexEvent")]
/**
* Dispatched at the start of a user interaction
* or when an animation starts.
*
* @eventType mx.events.FlexEvent.CHANGE_START
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="changeStart", type="mx.events.FlexEvent")]
/**
* Dispatched when the thumb is pressed and then moved by the mouse.
* This event is always preceded by a <code>thumbPress</code> event.
*
* @eventType spark.events.TrackBaseEvent.THUMB_DRAG
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="thumbDrag", type="spark.events.TrackBaseEvent")]
/**
* Dispatched when the thumb is pressed, meaning
* the user presses the mouse button over the thumb.
*
* @eventType spark.events.TrackBaseEvent.THUMB_PRESS
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="thumbPress", type="spark.events.TrackBaseEvent")]
/**
* Dispatched when the thumb is released,
* meaning the user releases the mouse button after
* a <code>thumbPress</code> event.
*
* @eventType spark.events.TrackBaseEvent.THUMB_RELEASE
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Event(name="thumbRelease", type="spark.events.TrackBaseEvent")]
/**
* Normal State
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[SkinState("normal")]
/**
* Disabled State
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[SkinState("disabled")]
/**
* Duration in milliseconds for a sliding animation
* when you click on the track to move a thumb. This style is
* used for both Sliders and Scrollbars. For Sliders, any click
* on the track will cause an animation using this style, as the thumb
* will move to the clicked position. For ScrollBars, this style is
* used only when shift-clicking on the track, which causes the thumb
* to move to the clicked position. Clicking on a ScrollBar track when
* the shift key is not pressed will result in paging behavior instead.
* The <code>smoothScrolling</code> style must also be set on the
* ScrollBar to get animated behavior when shift-clicking.
*
* <p>This duration is for an animation that covers the entire distance of the
* track; smaller distances will use a proportionally smaller duration.</p>
*
* @default 300
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
[Style(name="slideDuration", type="Number", format="Time", inherit="no")]
/**
* The TrackBase class is a base class for components with a track
* and one or more thumb buttons, such as Slider and ScrollBar. It
* declares two required skin parts: <code>thumb</code> and
* <code>track</code>.
* The TrackBase class also provides the code for
* dragging the thumb button, which is shared by the Slider and ScrollBar classes.
*
* @mxml
*
* <p>The <code>&lt;s:TrackBase&gt;</code> tag inherits all of the tag
* attributes of its superclass and adds the following tag attributes:</p>
*
* <pre>
* &lt;s:TrackBase
* <strong>Styles</strong>
* slideDuration="300"
*
* <strong>Events</strong>
* change="<i>No default</i>"
* changing="<i>No default</i>"
* thumbDrag="<i>No default</i>"
* thumbPress="<i>No default</i>"
* thumbRelease="<i>No default</i>"
* /&gt;
* </pre>
*
* @see spark.components.supportClasses.Slider
* @see spark.components.supportClasses.ScrollBar
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public class TrackBase extends Range
{
include "../../core/Version.as";
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public function TrackBase():void
{
super();
addEventListener(Event.ADDED_TO_STAGE, addedToStageHandler);
addEventListener(MouseEvent.MOUSE_DOWN, mouseDownHandler);
}
//--------------------------------------------------------------------------
//
// Skins
//
//--------------------------------------------------------------------------
[SkinPart(required="false")]
/**
* A skin part that defines a button
* that can be dragged along the track to increase or
* decrease the <code>value</code> property.
* Updates to the <code>value</code> property
* automatically update the position of the thumb button
* with respect to the track.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public var thumb:Button;
[SkinPart(required="false")]
/**
* A skin part that defines a button
* that, when pressed, sets the <code>value</code> property
* to the value corresponding with the current button position on the track.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
public var track:Button;
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
private var mouseDownTarget:DisplayObject;
//--------------------------------------------------------------------------
//
// Overridden properties: Range
//
//--------------------------------------------------------------------------
//---------------------------------
// maximum
//---------------------------------
[Inspectable(category="General", defaultValue="100.0")]
/**
* @private
* Overidden so that this property can be the source of a binding expression.
*/
override public function get maximum():Number
{
return super.maximum;
}
/**
* @private
*/
override public function set maximum(value:Number):void
{
if (value == super.maximum)
return;
super.maximum = value;
invalidateDisplayList();
}
//---------------------------------
// minimum
//---------------------------------
[Inspectable(category="General", defaultValue="0.0")]
/**
* @private
* Overidden so that this property can be the source of a binding expression.
*/
override public function get minimum():Number
{
return super.minimum;
}
/**
* @private
*/
override public function set minimum(value:Number):void
{
if (value == super.minimum)
return;
super.minimum = value;
invalidateDisplayList();
}
//---------------------------------
// scaleX
//---------------------------------
/**
* @private
* Overridden so that we can call invalidateDisplayList()
* when scaleX changes since the positioning of the thumb
* is incorrect when scaleX = 0 thanks to globalToLocal/localToGlobal().
*
* <p>We override it here in TrackBase instead of where the issue
* actually exists (HScrollBar, HSlider, VScrollBar, VSlider) because it
* is easier this way and most subclasses who rely on updateSkinDisplayList()
* will probably run in to this issue.</p>
*/
override public function set scaleX(value:Number):void
{
if (value == super.scaleX)
return;
// if we were scaled at 0 and are being set to something else,
// invalidate the display list (see comment above)
if (super.scaleX == 0)
invalidateDisplayList();
super.scaleX = value;
}
//---------------------------------
// scaleY
//---------------------------------
/**
* @private
* Overridden so that we can call invalidateDisplayList()
* when scaleY changes since the positioning of the thumb
* is incorrect when scaleY = 0 thanks to globalToLocal/localToGlobal().
*
* <p>We override it here in TrackBase instead of where the issue
* actually exists (HScrollBar, HSlider, VScrollBar, VSlider) because it
* is easier this way and most subclasses who rely on updateSkinDisplayList()
* will probably run in to this issue.</p>
*/
override public function set scaleY(value:Number):void
{
if (value == super.scaleY)
return;
// if we were scaled at 0 and are being set to something else,
// invalidate the display list (see comment above)
if (super.scaleY == 0)
invalidateDisplayList();
super.scaleY = value;
}
//---------------------------------
// value
//---------------------------------
[Bindable(event="valueCommit")] // Warning: must match the Bindable tag in Range
[Inspectable(category="General", defaultValue="0.0")]
/**
* @private
* Overidden so that this property can be the source of a binding expression.
*/
override public function get value():Number
{
return super.value;
}
/**
* @private
*/
override public function set value(newValue:Number):void
{
if (newValue == super.value)
return;
super.value = newValue;
invalidateDisplayList();
}
/**
* @private
*/
override protected function setValue(value:Number):void
{
super.setValue(value);
invalidateDisplayList();
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* Converts a track-relative x,y pixel location into a value between
* the minimum and maximum, inclusive.
*
* <p>TrackBase subclasses must override this method and perform conversions
* that take their own geometry into account.
*
* For example, a vertical slider might compute a value like this:
* <pre>
* return (y / track.height) * (maximum - minimum);
* </pre>
* </p>
*
* <p>By default, this method returns <code>minimum</code>.</p>
*
* @param x The x coordinate of the location relative to the track's origin.
* @param y The y coordinate of the location relative to the track's origin.
* @return A value between the minimum and maximum, inclusive.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
protected function pointToValue(x:Number, y:Number):Number
{
return minimum;
}
/**
* @private
*/
override public function changeValueByStep(increase:Boolean = true):void
{
var prevValue:Number = this.value;
super.changeValueByStep(increase);
if (value != prevValue)
dispatchEvent(new Event(Event.CHANGE));
}
/**
* @private
*/
override protected function getCurrentSkinState():String
{
return enabled ? "normal" : "disabled";
}
/**
* @private
* Warning: the goal of the listeners added here (and removed below) is to
* give the TrackBase a change to fixup the thumb's size and position
* after the skin's BasicLayout has run. This particular implementation
* is a hack and it begs a solution to the general problem of what we've
* called "cooperative layout". More about that here:
* http://opensource.adobe.com/wiki/display/flexsdk/Cooperative+Subtree+Layout
*/
override protected function partAdded(partName:String, instance:Object):void
{
super.partAdded(partName, instance);
if (instance == thumb)
{
thumb.addEventListener(MouseEvent.MOUSE_DOWN, thumb_mouseDownHandler);
thumb.addEventListener(TouchInteractionEvent.TOUCH_INTERACTION_STARTING, thumb_touchInteractionStartingHandler);
thumb.addEventListener(ResizeEvent.RESIZE, thumb_resizeHandler);
thumb.addEventListener(FlexEvent.UPDATE_COMPLETE, thumb_updateCompleteHandler);
thumb.stickyHighlighting = true;
}
else if (instance == track)
{
track.addEventListener(ResizeEvent.RESIZE, track_resizeHandler);
// track is only clickable if in mouse interactionMode
if (getStyle("interactionMode") == InteractionMode.MOUSE)
track.addEventListener(MouseEvent.MOUSE_DOWN, track_mouseDownHandler);
}
}
/**
* @private
*/
override protected function partRemoved(partName:String, instance:Object):void
{
super.partRemoved(partName, instance);
if (instance == thumb)
{
thumb.removeEventListener(MouseEvent.MOUSE_DOWN, thumb_mouseDownHandler);
thumb.removeEventListener(TouchInteractionEvent.TOUCH_INTERACTION_STARTING, thumb_touchInteractionStartingHandler);
thumb.removeEventListener(ResizeEvent.RESIZE, thumb_resizeHandler);
thumb.removeEventListener(FlexEvent.UPDATE_COMPLETE, thumb_updateCompleteHandler);
}
else if (instance == track)
{
track.removeEventListener(MouseEvent.MOUSE_DOWN, track_mouseDownHandler);
track.removeEventListener(ResizeEvent.RESIZE, track_resizeHandler);
}
}
/**
* @private
*/
override public function styleChanged(styleName:String):void
{
var allStyles:Boolean = styleName == null || styleName == "styleName";
super.styleChanged(styleName);
if (allStyles || styleName == "interactionMode")
{
if (track)
{
// track is only clickable if in mouse interactionMode
if (getStyle("interactionMode") == InteractionMode.MOUSE)
track.addEventListener(MouseEvent.MOUSE_DOWN, track_mouseDownHandler);
else
track.removeEventListener(MouseEvent.MOUSE_DOWN, track_mouseDownHandler);
}
}
}
/**
* @private
* If the component is in focus, then it should respond to mouseWheel events. We listen to these
* events on systemManager in the capture phase because this behavior should have the highest priority.
*/
override protected function focusInHandler(event:FocusEvent):void
{
super.focusInHandler(event);
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_WHEEL, system_mouseWheelHandler, true);
}
/**
* @private
*/
override protected function focusOutHandler(event:FocusEvent):void
{
super.focusOutHandler(event);
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_WHEEL, system_mouseWheelHandler, true);
}
/**
* @private
*/
override protected function updateDisplayList(w:Number, h:Number):void
{
super.updateDisplayList(w, h);
updateSkinDisplayList();
}
//--------------------------------------------------------------------------
//
// Event Handlers
//
//--------------------------------------------------------------------------
/**
* @private
* Location of the mouse down event on the thumb, relative to the thumb's origin.
* Used to update the value property when the mouse is dragged.
*/
private var clickOffset:Point;
/**
* Sets the bounds of skin parts, typically the thumb, whose geometry isn't fully
* specified by the skin's layout.
*
* <p>Most subclasses override this method to update the thumb's size, position, and
* visibility, based on the <code>minimum</code>, <code>maximum</code>, and <code>value</code> properties. </p>
*
* <p>By default, this method does nothing.</p>
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*
*/
protected function updateSkinDisplayList():void {}
/**
* @private
* Redraw whenever added to the stage to ensure the calculations
* in updateSkinDisplayList() are correct.
*/
private function addedToStageHandler(event:Event):void
{
updateSkinDisplayList();
}
/**
* @private
*/
private function track_resizeHandler(event:Event):void
{
updateSkinDisplayList();
}
/**
* @private
*/
private function thumb_resizeHandler(event:Event):void
{
updateSkinDisplayList();
}
/**
* @private
*/
private function thumb_updateCompleteHandler(event:Event):void
{
updateSkinDisplayList();
thumb.removeEventListener(FlexEvent.UPDATE_COMPLETE, thumb_updateCompleteHandler);
}
/**
* @private
* Handles the <code>mouseWheel</code> event when the component is in focus. The thumb is
* moved by the amount of the mouse event delta multiplied by the <code>stepSize</code>.
*
* @langversion 3.0
* @playerversion Flash 10
* @playerversion AIR 1.5
* @productversion Flex 4
*/
protected function system_mouseWheelHandler(event:MouseEvent):void
{
if (!event.isDefaultPrevented())
{
var newValue:Number = nearestValidValue(value + event.delta * stepSize, stepSize);
setValue(newValue);
dispatchEvent(new Event(Event.CHANGE));
event.preventDefault();
}
}
//---------------------------------
// Thumb dragging handlers
//---------------------------------
/**
* @private
* Handle mouse-down events on the scroll thumb. Records
* the mouse down point in clickOffset.
*/
protected function thumb_mouseDownHandler(event:MouseEvent):void
{
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_MOVE,
system_mouseMoveHandler, true);
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_UP,
system_mouseUpHandler, true);
systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE,
system_mouseUpHandler);
clickOffset = thumb.globalToLocal(new Point(event.stageX, event.stageY));
dispatchEvent(new TrackBaseEvent(TrackBaseEvent.THUMB_PRESS));
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_START));
}
/**
* @private
*/
protected function thumb_touchInteractionStartingHandler(event:TouchInteractionEvent):void
{
// cancel this event b/c I want to control it
event.preventDefault();
}
/**
* @private
* Capture mouse-move events anywhere on or off the stage.
* First, we calculate the new value based on the new position
* using calculateNewValue(). Then, we move the thumb to
* the new value's position. Last, we set the value and
* dispatch a "change" event if the value changes.
*/
protected function system_mouseMoveHandler(event:MouseEvent):void
{
if (!track)
return;
var p:Point = track.globalToLocal(new Point(event.stageX, event.stageY));
var newValue:Number = pointToValue(p.x - clickOffset.x, p.y - clickOffset.y);
newValue = nearestValidValue(newValue, snapInterval);
if (newValue != value)
{
setValue(newValue);
dispatchEvent(new TrackBaseEvent(TrackBaseEvent.THUMB_DRAG));
dispatchEvent(new Event(Event.CHANGE));
}
event.updateAfterEvent();
}
/**
* @private
* Handle mouse-up events anywhere on or off the stage.
*/
protected function system_mouseUpHandler(event:Event):void
{
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_MOVE,
system_mouseMoveHandler, true);
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_UP,
system_mouseUpHandler, true);
systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE,
system_mouseUpHandler);
dispatchEvent(new TrackBaseEvent(TrackBaseEvent.THUMB_RELEASE));
dispatchEvent(new FlexEvent(FlexEvent.CHANGE_END));
}
//---------------------------------
// Track down handlers
//---------------------------------
/**
* @private
* Handle mouse-down events for the scroll track. Subclasses
* should override this method if they want the track to
* recognize mouse clicks on the track.
*/
protected function track_mouseDownHandler(event:MouseEvent):void { }
//---------------------------------
// Mouse click handlers
//---------------------------------
/**
* @private
* Capture any mouse down event and listen for a mouse up event
*/
private function mouseDownHandler(event:MouseEvent):void
{
systemManager.getSandboxRoot().addEventListener(MouseEvent.MOUSE_UP,
system_mouseUpSomewhereHandler, true);
systemManager.getSandboxRoot().addEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE,
system_mouseUpSomewhereHandler);
mouseDownTarget = DisplayObject(event.target);
}
/**
* @private
*/
private function system_mouseUpSomewhereHandler(event:Event):void
{
systemManager.getSandboxRoot().removeEventListener(MouseEvent.MOUSE_UP,
system_mouseUpSomewhereHandler, true);
systemManager.getSandboxRoot().removeEventListener(SandboxMouseEvent.MOUSE_UP_SOMEWHERE,
system_mouseUpSomewhereHandler);
// If we got a mouse down followed by a mouse up on a different target in the skin,
// we want to dispatch a click event.
if (mouseDownTarget != event.target && event is MouseEvent && contains(DisplayObject(event.target)))
{
var mEvent:MouseEvent = event as MouseEvent;
// Convert the mouse coordinates from the target to the TrackBase
var mousePoint:Point = new Point(mEvent.localX, mEvent.localY);
mousePoint = globalToLocal(DisplayObject(event.target).localToGlobal(mousePoint));
dispatchEvent(new MouseEvent(MouseEvent.CLICK, mEvent.bubbles, mEvent.cancelable, mousePoint.x,
mousePoint.y, mEvent.relatedObject, mEvent.ctrlKey, mEvent.altKey,
mEvent.shiftKey, mEvent.buttonDown, mEvent.delta));
}
mouseDownTarget = null;
}
}
}