blob: 2939b7412125b14f0f23b9eaccbdbe8f2e02cc9c [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 mx.collections
{
import flash.events.EventDispatcher;
import flash.utils.getQualifiedClassName;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.PropertyChangeEvent;
import mx.events.PropertyChangeEventKind;
import mx.managers.ISystemManager;
import mx.managers.SystemManager;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
import mx.utils.IXMLNotifiable;
import mx.utils.XMLNotifier;
import mx.utils.UIDUtil;
//--------------------------------------
// Events
//--------------------------------------
/**
* Dispatched when the IList has been updated in some way.
*
* @eventType mx.events.CollectionEvent.COLLECTION_CHANGE
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="collectionChange", type="mx.events.CollectionEvent")]
//--------------------------------------
// Other metadata
//--------------------------------------
[ExcludeClass]
[ResourceBundle("collections")]
/**
* @private
* A simple implementation of IList that uses a backing XMLList.
* No ItemPendingErrors since the data is always local.
*/
public class XMLListAdapter extends EventDispatcher implements IList, IXMLNotifiable
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Construct a new XMLListAdapter using the specified XMLList as its source.
* If no source is specified an empty XMLList will be used.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function XMLListAdapter(source:XMLList = null)
{
super();
disableEvents();
this.source = source;
enableEvents();
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
* Used for accessing localized Error messages.
*/
private var resourceManager:IResourceManager =
ResourceManager.getInstance();
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// length
//----------------------------------
/**
* The number of items in this list.
*
* @return -1 if the length is unknown while 0 means no items
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get length():int
{
return source.length();
}
//----------------------------------
// source
//----------------------------------
private var _source:XMLList;
/**
* The source XMLList for this XMLListAdapter.
* Any changes done through the IList interface will be reflected in the
* source XMLList.
* If no source XMLList was supplied the XMLListAdapter will create one
* internally.
* Changes made directly to the underlying XMLList (e.g., calling
* <code>delete theList[someIndex]</code> will not cause <code>CollectionEvents</code>
* to be dispatched.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get source():XMLList
{
return _source;
}
public function set source(s:XMLList):void
{
var i:int;
var len:int;
if (_source && _source.length())
{
len = _source.length();
for (i = 0; i < len; i++)
{
stopTrackUpdates(_source[i]);
}
}
_source = s ? s : XMLList(<></>);
// get a seed UID. XML seems to have problems cleaning up
// UIDs, so this makes UIDs more similar and uses less
// memory
seedUID = UIDUtil.createUID();
uidCounter = 0;
len = _source.length();
for (i = 0; i < len; i++)
{
startTrackUpdates(_source[i], seedUID + uidCounter.toString());
uidCounter++;
}
if (_dispatchEvents == 0)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.RESET;
dispatchEvent(event);
}
}
//--------------------------------------------------------------------------
//
// Methods
//
//--------------------------------------------------------------------------
/**
* Add the specified item to the end of the list. Unlike addItemAt, this
* does not change the source memory pointer.
*
* @param item the item to add
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addItem(item:Object):void
{
var message:String;
if (!(item is XML) && !(item is XMLList && item.length() == 1))
{
message = resourceManager.getString(
"collections", "invalidType");
throw new Error(message);
}
setBusy();
source[length] = item;
startTrackUpdates(item, seedUID + uidCounter.toString());
uidCounter++;
if (_dispatchEvents == 0)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.ADD;
event.items.push(item);
event.location = source.length()-1;
dispatchEvent(event);
}
clearBusy();
}
/**
* Add the item at the specified index. Any item that was after
* this index is moved out by one. If the list is shorter than
* the specified index it will grow to accommodate the new item.
*
* The source array may change, and changes made to it may not be
* tracked after this operation if you access it directly.
*
* @param item the item to place at the index
* @param index the index at which to place the item
* @throws RangeError if index is less than 0
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addItemAt(item:Object, index:int):void
{
var message:String;
if (index < 0 || index > length)
{
message = resourceManager.getString(
"collections", "outOfBounds", [ index ]);
throw new RangeError(message);
}
if (!(item is XML) && !(item is XMLList && item.length() == 1))
{
message = resourceManager.getString(
"collections", "invalidType");
throw new Error(message);
}
setBusy();
// Replace the current source XMLList with a new list in order to add
// the item to the list. This is needed to maintain the structure of
// the new item added to the list and avoid an FP bug.
if (length > 0)
{
var newSource:XMLList = new XMLList();
for (var i:uint = 0; i <= (length); i++)
{
if (i < index)
{
newSource[i] = source[i];
}
else if (i == index)
{
newSource[i] = item;
}
else if (i > index)
{
newSource[i] = source[i-1];
}
}
source = newSource;
}
else
{
source[index] = item;
}
startTrackUpdates(item, seedUID + uidCounter.toString());
uidCounter++;
if (_dispatchEvents == 0)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.ADD;
event.items.push(item);
event.location = index;
dispatchEvent(event);
}
clearBusy();
}
/**
* Get the item at the specified index.
*
* @param index the index in the list from which to retrieve the item
* @param prefetch int indicating both the direction and amount of items
* to fetch during the request should the item not be local. It is
* not used at this time.
* @return the item at that index, null if there is none
* @throws ItemPendingError if the data for that index needs to be
* loaded from a remote location
* @throws RangeError if the index < 0 or index >= length
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function getItemAt(index:int, prefetch:int = 0):Object
{
if (index < 0 || index >= length)
{
var message:String = resourceManager.getString(
"collections", "outOfBounds", [ index ]);
throw new RangeError(message);
}
return source[index];
}
/**
* Return the index of the item if it is in the list such that
* getItemAt(index) == item. Note: unlike IViewCursor.findXXX
* <code>getItemIndex</code> cannot take a representative object, it is
* searching for an exact match.
*
* @param item the item to find
* @return the index of the item, -1 if the item is not in the list.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function getItemIndex(item:Object):int
{
if (item is XML && source.contains(XML(item)))
{
var len:int = length;
for (var i:int = 0; i < len; i++)
{
var search:Object = source[i];
if (search === item)
{
return i;
}
}
}
return -1;
}
/**
* Notify the view that an item has been updated. This is useful if the
* contents of the view do not implement <code>IEventDispatcher</code>
* and dispatches a <code>PropertyChangeEvent</code>. If a property
* is specified the view may be able to optimize its notification mechanism.
* Otherwise it may choose to simply refresh the whole view.
*
* @param item The item within the view that was updated.
*
* @param property A String, QName, or int
* specifying the property that was updated.
*
* @param oldValue The old value of that property.
* (If property was null, this can be the old value of the item.)
*
* @param newValue The new value of that property.
* (If property was null, there's no need to specify this
* as the item is assumed to be the new value.)
*
* @see mx.events.CollectionEvent
* @see mx.core.IPropertyChangeNotifier
* @see mx.events.PropertyChangeEvent
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function itemUpdated(item:Object, property:Object = null,
oldValue:Object = null,
newValue:Object = null):void
{
var event:PropertyChangeEvent =
new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
event.kind = PropertyChangeEventKind.UPDATE;
event.source = item;
event.property = property;
event.oldValue = oldValue;
event.newValue = newValue;
itemUpdateHandler(event);
}
/**
* Remove all items from the list.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeAll():void
{
if (length > 0)
{
for (var i:int=length - 1; i >= 0; i--)
{
stopTrackUpdates(source[i]);
}
source = new XMLList();
if (_dispatchEvents == 0)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.RESET;
dispatchEvent(event);
}
}
}
/**
* Removes the specified item from this list, should it exist.
* Relies on ArrayList implementation
*
* @param item Object reference to the item that should be removed.
* @return Boolean indicating if the item was removed.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Apache Flex 4.10
*/
public function removeItem( item:Object ):Boolean
{
var _item:Object = removeItemAt[getItemIndex(item)];
return _item != null;
}
/**
* Remove the item at the specified index and return it. Any items
* that were after this index are now one index earlier.
*
* @param index the index from which to remove the item
* @return the item that was removed
* @throws RangeError is index is less than 0 or greater than length
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeItemAt(index:int):Object
{
if (index < 0 || index >= length)
{
var message:String = resourceManager.getString(
"collections", "outOfBounds", [ index ]);
throw new RangeError(message);
}
setBusy();
var removed:Object = source[index];
stopTrackUpdates(removed);
// loop through array to remove that index.
if (length > 1)
{
var newSource:XMLList = new XMLList();
for (var i:uint = 0; i < length; i++)
{
if (i < index)
{
newSource[i] = source[i];
}
else if (i > index)
{
newSource[i-1] = source[i];
}
}
source = newSource;
}
else
{
source = new XMLList();
}
if (_dispatchEvents == 0)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.REMOVE;
event.location = index;
event.items.push(removed);
dispatchEvent(event);
}
clearBusy();
return removed;
}
/**
* Place the item at the specified index. If an item was already
* at that index the new item will replace it and it will be returned.
* If the list is shorter than the specified index it will grow to
* to accomodate the new item.
*
* @param item the new value for the index
* @param index the index at which to place the item
* @return the item that was replaced, null if none
* @throws RangeError if index is less than 0
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function setItemAt(item:Object, index:int):Object
{
if (index < 0 || index >= length)
{
var message:String = resourceManager.getString(
"collections", "outOfBounds", [ index ]);
throw new RangeError(message);
}
var oldItem:Object = source[index];
source[index] = item;
stopTrackUpdates(oldItem);
startTrackUpdates(item, seedUID + uidCounter.toString());
uidCounter++;
if (_dispatchEvents == 0)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.REPLACE;
var updateInfo:PropertyChangeEvent =
new PropertyChangeEvent(PropertyChangeEvent.PROPERTY_CHANGE);
updateInfo.kind = PropertyChangeEventKind.UPDATE;
updateInfo.oldValue = oldItem;
updateInfo.newValue = item;
event.location = index;
event.items.push(updateInfo);
dispatchEvent(event);
}
return oldItem;
}
/**
* Return an Array that is populated in the same order as the IList
* implementation.
*
* @throws ItemPendingError if the data is not yet completely loaded
* from a remote location
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function toArray():Array
{
var s:XMLList = source;
var len:int = s.length();
var ret:Array = [];
for (var i:int = 0; i < len; i++)
{
ret[i] = s[i];
}
return ret;
}
/**
* Pretty prints the contents of this XMLListAdapter to a string and returns it.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
override public function toString():String
{
if (source)
return source.toString();
else
return getQualifiedClassName(this);
}
/**
* True if we're processing a addItem or removeItem call
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function busy():Boolean
{
return (_busy != 0)
}
//--------------------------------------------------------------------------
//
// Internal Methods
//
//--------------------------------------------------------------------------
/**
* Enables event dispatch for this list.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
protected function enableEvents():void
{
_dispatchEvents++;
if (_dispatchEvents > 0)
_dispatchEvents = 0;
}
/**
* Disables event dispatch for this list.
* To re-enable events call enableEvents(), enableEvents() must be called
* a matching number of times as disableEvents().
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
protected function disableEvents():void
{
_dispatchEvents--;
}
/**
* clears busy flag
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function clearBusy():void
{
_busy++;
if (_busy > 0)
_busy = 0;
}
/**
* Sets busy flag. Tree DP's check it so they
* know whether to fake events for it or not.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function setBusy():void
{
_busy--;
}
/**
* Called whenever any of the contained items in the list fire an
* ObjectChange event.
* Wraps it in a CollectionEventKind.UPDATE.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
protected function itemUpdateHandler(event:PropertyChangeEvent):void
{
if (_dispatchEvents == 0)
{
var updateEvent:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
updateEvent.kind = CollectionEventKind.UPDATE;
updateEvent.items.push(event);
dispatchEvent(updateEvent);
}
}
/**
* Called whenever an XML object contained in our list is updated
* in some way. The initial implementation stab is very lenient,
* any changeType will cause an update no matter how much further down
* in a hierarchy.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function xmlNotification(currentTarget:Object,
type:String,
target:Object,
value:Object,
detail:Object):void
{
var prop:String;
var oldValue:Object;
var newValue:Object;
if (currentTarget === target)
{
switch(type)
{
case "attributeAdded":
{
prop = "@" + String(value);
newValue = detail;
break;
}
case "attributeChanged":
{
prop = "@" + String(value);
oldValue = detail;
newValue = target[prop];
break;
}
case "attributeRemoved":
{
prop = "@" + String(value);
oldValue = detail;
break;
}
case "nodeAdded":
{
prop = value.localName();
newValue = value;
break;
}
case "nodeChanged":
{
prop = value.localName();
oldValue = detail;
newValue = value;
break;
}
case "nodeRemoved":
{
prop = value.localName();
oldValue = value;
break;
}
case "textSet":
{
prop = String(value);
newValue = String(target[prop]);
oldValue = detail;
break;
}
default:
{
break;
}
}
}
else if (type == "textSet")
{
var par:* = target.parent();
if (par != undefined)
{
var gpar:* = par.parent();
if (gpar === currentTarget)
{
prop = par.name().toString();
newValue = value;
oldValue = detail;
}
}
}
itemUpdated(currentTarget, prop, oldValue, newValue);
}
/**
* This is called by addItemAt and when the source is initially
* assigned.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
protected function startTrackUpdates(item:Object, uid:String):void
{
XMLNotifier.getInstance().watchXML(item, this, uid);
}
/**
* This is called by removeItemAt, removeAll, and before a new
* source is assigned.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
protected function stopTrackUpdates(item:Object):void
{
XMLNotifier.getInstance().unwatchXML(item, this);
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* indicates if events should be dispatched.
* calls to enableEvents() and disableEvents() effect the value when == 0
* events should be dispatched.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private var _dispatchEvents:int = 0;
/**
* non-zero if we're processing an addItem or removeItem
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private var _busy:int = 0;
private var seedUID:String;
private var uidCounter:int = 0;
}
}