package org.osflash.signals
{
import flash.errors.IllegalOperationError;
import flash.utils.Dictionary;
import flash.utils.getQualifiedClassName;
/**
* Allows the valueClasses to be set in MXML, e.g.
* {[String, uint]}
*/
[DefaultProperty("valueClasses")]
/**
* Signal dispatches events to multiple listeners.
* It is inspired by C# events and delegates, and by
* signals and slots
* in Qt.
* A Signal adds event dispatching functionality through composition and interfaces,
* rather than inheriting from a dispatcher.
*
* Project home: http://github.com/robertpenner/as3-signals/
*/
public class Signal implements ISignalOwner, IDispatcher
{
protected var _valueClasses:Array; // of Class
protected var slots:Array; // of Slot
protected var slotsNeedCloning:Boolean = false;
/**
* Creates a Signal instance to dispatch value objects.
* @param valueClasses Any number of class references that enable type checks in dispatch().
* For example, new Signal(String, uint)
* would allow: signal.dispatch("the Answer", 42)
* but not: signal.dispatch(true, 42.5)
* nor: signal.dispatch()
*
* NOTE: Subclasses cannot call super.apply(null, valueClasses),
* but this constructor has logic to support super(valueClasses).
*/
public function Signal(...valueClasses)
{
slots = [];
// Cannot use super.apply(null, valueClasses), so allow the subclass to call super(valueClasses).
if (valueClasses.length == 1 && valueClasses[0] is Array)
valueClasses = valueClasses[0];
this.valueClasses = valueClasses;
}
/** @inheritDoc */
[ArrayElementType("Class")]
public function get valueClasses():Array { return _valueClasses; }
/** @inheritDoc */
public function set valueClasses(value:Array):void
{
// Clone so the Array cannot be affected from outside.
_valueClasses = value ? value.slice() : [];
for (var i:int = _valueClasses.length; i--; )
{
if (!(_valueClasses[i] is Class))
{
throw new ArgumentError('Invalid valueClasses argument: item at index ' + i
+ ' should be a Class but was:<' + _valueClasses[i] + '>.' + getQualifiedClassName(_valueClasses[i]));
}
}
}
/** @inheritDoc */
public function get numListeners():uint { return slots.length; }
/** @inheritDoc */
//TODO: @throws
public function add(listener:Function):Function
{
registerListener(listener);
return listener;
}
/** @inheritDoc */
public function addOnce(listener:Function):Function
{
registerListener(listener, true);
return listener;
}
/** @inheritDoc */
public function remove(listener:Function):Function
{
if (indexOfListener(listener) == -1) return listener;
if (slotsNeedCloning)
{
slots = slots.slice();
slotsNeedCloning = false;
}
slots.splice(indexOfListener(listener), 1);
return listener;
}
/** @inheritDoc */
public function removeAll():void
{
// Looping backwards is more efficient when removing array items.
for (var i:uint = slots.length; i--; )
{
// This is the "proper" type-safe way, but perhaps not the fastest.
// TODO: Test for speed.
remove(Slot(slots[i]).listener);
}
}
/** @inheritDoc */
public function dispatch(...valueObjects):void
{
// Validate value objects against pre-defined value classes.
var valueObject:Object;
var valueClass:Class;
const numValueClasses:uint = _valueClasses.length;
if (valueObjects.length < numValueClasses)
{
throw new ArgumentError('Incorrect number of arguments. Expected at least ' + numValueClasses + ' but received ' + valueObjects.length + '.');
}
for (var i:int = 0; i < numValueClasses; i++)
{
// null is allowed to pass through.
if ( (valueObject = valueObjects[i]) === null
|| valueObject is (valueClass = _valueClasses[i]) )
continue;
throw new ArgumentError('Value object <' + valueObject
+ '> is not an instance of <' + valueClass + '>.');
}
//// Call listeners.
if (slots.length)
{
// During a dispatch, add() and remove() should clone listeners array instead of modifying it.
slotsNeedCloning = true;
var slot:Slot;
switch (valueObjects.length)
{
case 0:
for each (slot in slots)
{
slot.execute0();
}
break;
case 1:
const singleValue:Object = valueObjects[0];
for each (slot in slots)
{
slot.execute1(singleValue);
}
break;
case 2:
const value1:Object = valueObjects[0];
const value2:Object = valueObjects[1];
for each (slot in slots)
{
slot.execute2(value1, value2);
}
break;
default:
for each (slot in slots)
{
slot.execute(valueObjects);
}
}
slotsNeedCloning = false;
}
}
protected function indexOfListener(listener:Function):int
{
for (var i:int = slots.length; i--; )
{
if (Slot(slots[i]).listener == listener) return i;
}
return -1;
}
protected function registerListener(listener:Function, once:Boolean = false):void
{
// function.length is the number of arguments.
if (listener.length < _valueClasses.length)
{
const argumentString:String = (listener.length == 1) ? 'argument' : 'arguments';
throw new ArgumentError('Listener has '+listener.length+' '+argumentString+' but it needs at least '+_valueClasses.length+' to match the given value classes.');
}
const slot:Slot = new Slot(listener, once, this);
// If there are no previous listeners, add the first one as quickly as possible.
if (!slots.length)
{
slots[0] = slot;
return;
}
var prevListenerIndex:int = indexOfListener(listener);
if (prevListenerIndex >= 0)
{
// If the listener was previously added, definitely don't add it again.
// But throw an exception in some cases, as the error messages explain.
var prevSlot:Slot = Slot(slots[prevListenerIndex]);
if (prevSlot.once && !once)
{
throw new IllegalOperationError('You cannot addOnce() then add() the same listener without removing the relationship first.');
}
else if (!prevSlot.once && once)
{
throw new IllegalOperationError('You cannot add() then addOnce() the same listener without removing the relationship first.');
}
// Listener was already added, so do nothing.
return;
}
if (slotsNeedCloning)
{
slots = slots.slice();
slotsNeedCloning = false;
}
// Faster than push().
slots[slots.length] = slot;
}
}
}