blob: 301796b694fe359e34fa29d1241e89678e8db4d8 [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.Event;
import flash.events.EventDispatcher;
import flash.utils.Proxy;
import flash.utils.flash_proxy;
import flash.utils.getQualifiedClassName;
import mx.binding.utils.BindingUtils;
import mx.binding.utils.ChangeWatcher;
import mx.collections.errors.CollectionViewError;
import mx.collections.errors.ItemPendingError;
import mx.collections.errors.SortError;
import mx.core.IMXMLObject;
import mx.core.mx_internal;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.events.PropertyChangeEvent;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
import mx.utils.ObjectUtil;
use namespace mx_internal;
/**
* Dispatched when the ICollectionView 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")]
[ResourceBundle("collections")]
/**
* The ListCollectionView class adds the properties and methods of the
* <code>ICollectionView</code> interface to an object that conforms to the
* <code>IList</code> interface. As a result, you can pass an object of this class
* to anything that requires an <code>IList</code> or <code>ICollectionView</code>.
*
* <p>This class also lets you use [ ] array notation
* to access the <code>getItemAt()</code> and <code>setItemAt()</code> methods.
* If you use code such as <code>myListCollectionView[index]</code>
* Flex calls the <code>myListCollectionView</code> object's
* <code>getItemAt()</code> or <code>setItemAt()</code> method.</p>
*
* @mxml
*
* <p>The <code>&lt;mx:ListCollectionView&gt;</code> has the following attributes,
* which all of its subclasses inherit:</p>
*
* <pre>
* &lt;mx:ListCollectionView
* <b>Properties</b>
* filterFunction="null"
* list="null"
* sort="null"
* <b>Events</b>
* collectionChange="<i>No default</i>"
* /&gt;
* </pre>
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public class ListCollectionView extends Proxy
implements ICollectionView, IList, IMXMLObject
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Private variables
//
//--------------------------------------------------------------------------
/**
* @private
* Change watcher for complex sort fields.
*/
private var _complexFieldWatcher:ComplexFieldChangeWatcher;
/**
* @private
* Internal event dispatcher.
*/
private var eventDispatcher:EventDispatcher;
/**
* @private
* Revisions are used for bookmark maintenace,
* see getBookmark() and getBookmarkIndex() along with reset().
*/
private var revision:int;
/**
* @private
* Used internally for managing disableAutoUpdate and enableAutoUpdate
* calls. disableAutoUpdate increments the counter, enable decrements.
* When the counter reaches 0 handlePendingUpdates is called.
*/
private var autoUpdateCounter:int;
/**
* @private
* Any update events that occured while autoUpdateCounter > 0
* are stored here.
* This may be null when there are no updates.
*/
private var pendingUpdates:Array;
/**
* @private
* Flag that indicates whether a RESET type of collectionChange
* event should be emitted when reset() is called.
*/
mx_internal var dispatchResetEvent:Boolean = true;
/**
* @private
* Used for accessing localized Error messages.
*/
private var resourceManager:IResourceManager =
ResourceManager.getInstance();
//--------------------------------------------------------------------------
//
// Protected variables
//
//--------------------------------------------------------------------------
/**
* When the view is sorted or filtered the <code>localIndex</code> property
* contains an array of items in the sorted or filtered (ordered, reduced)
* view, in the sorted order.
* The ListCollectionView class uses this property to access the items in
* the view.
* The <code>localIndex</code> property should never contain anything
* that is not in the source, but may not have everything in the source.
* This property is <code>null</code> when there is no sort.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
protected var localIndex:Array;
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* The ListCollectionView constructor.
*
* @param list the IList this ListCollectionView is meant to wrap.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function ListCollectionView(list:IList = null)
{
super();
eventDispatcher = new EventDispatcher(this);
this.list = list;
}
/**
* Called automatically by the MXML compiler when the ListCollectionView
* is created using an MXML tag.
* If you create the ListCollectionView through ActionScript, you
* must call this method passing in the MXML document and
* <code>null</code> for the <code>id</code>.
*
* @param document The MXML document containing this ListCollectionView.
*
* @param id Ignored.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function initialized(document:Object, id:String):void
{
refresh();
}
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
//----------------------------------
// length
//----------------------------------
[Bindable("collectionChange")]
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get length():int
{
if (localIndex)
{
return localIndex.length;
}
else if (list)
{
return list.length;
}
else
{
return 0;
}
}
//----------------------------------
// list
//----------------------------------
/**
* @private
* Storage for the list property.
*/
private var _list:IList;
[Inspectable(category="General")]
[Bindable("listChanged")]
/**
* The IList that this collection view wraps.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get list():IList
{
return _list;
}
/**
* @private
*/
public function set list(value:IList):void
{
if (_list != value)
{
var oldHasItems:Boolean;
var newHasItems:Boolean;
if (_list)
{
_list.removeEventListener(CollectionEvent.COLLECTION_CHANGE,
listChangeHandler);
oldHasItems = _list.length > 0;
}
_list = value;
if (_list)
{
// weak listeners to collections and dataproviders
_list.addEventListener(CollectionEvent.COLLECTION_CHANGE,
listChangeHandler, false, 0, true);
newHasItems = _list.length > 0;
}
if (oldHasItems || newHasItems)
reset();
dispatchEvent(new Event("listChanged"));
}
}
//----------------------------------
// filterFunction
//----------------------------------
/**
* @private
* Storage for the filterFunction property.
*/
private var _filterFunction:Function;
[Bindable("filterFunctionChanged")]
[Inspectable(category="General")]
/**
* @inheritDoc
*
* @see #refresh()
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get filterFunction():Function
{
return _filterFunction;
}
/**
* @private
*/
public function set filterFunction(f:Function):void
{
_filterFunction = f;
dispatchEvent(new Event("filterFunctionChanged"));
}
//----------------------------------
// sort
//----------------------------------
/**
* @private
* Storage for the sort property.
*/
private var _sort:ISort;
[Bindable("sortChanged")]
[Inspectable(category="General")]
/**
* @inheritDoc
*
* @see #refresh()
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get sort():ISort
{
return _sort;
}
/**
* @private
*/
public function set sort(value:ISort):void
{
if(_sort != value)
{
stopWatchingForComplexFieldsChanges();
_sort = value;
startWatchingForComplexFieldsChanges();
dispatchEvent(new Event("sortChanged"));
}
}
//--------------------------------------------------------------------------
//
// ICollectionView Methods
//
//--------------------------------------------------------------------------
/**
* @inheritDoc
*
* @see #enableAutoUpdate()
* @see mx.events.CollectionEvent
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function contains(item:Object):Boolean
{
return getItemIndex(item) != -1;
}
/**
* @inheritDoc
*
* @see mx.collections.ICollectionView#enableAutoUpdate()
* @see mx.events.CollectionEvent
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function disableAutoUpdate():void
{
autoUpdateCounter++;
}
/**
* @inheritDoc
*
* @see mx.collections.ICollectionView#disableAutoUpdate()
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function enableAutoUpdate():void
{
if (autoUpdateCounter > 0)
{
autoUpdateCounter--;
if (autoUpdateCounter == 0)
{
handlePendingUpdates();
}
}
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function createCursor():IViewCursor
{
return new ListCollectionViewCursor(this);
}
/**
* @inheritDoc
*
* @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
{
list.itemUpdated(item, property, oldValue, newValue);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function refresh():Boolean
{
return internalRefresh(true);
}
//--------------------------------------------------------------------------
//
// IList Methods
//
//--------------------------------------------------------------------------
[Bindable("collectionChange")]
/**
* @inheritDoc
*
* @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);
}
if (localIndex)
{
return localIndex[index];
}
else if (list)
{
return list.getItemAt(index, prefetch);
}
return null;
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function setItemAt(item:Object, index:int):Object
{
if (index < 0 || !list || index >= length)
{
var message:String = resourceManager.getString(
"collections", "outOfBounds", [ index ]);
throw new RangeError(message);
}
var listIndex:int = index;
if (localIndex)
{
if (index > localIndex.length)
{
listIndex = list.length;
}
else
{
var oldItem:Object = localIndex[index];
// FIXME fails on duplicates
listIndex = list.getItemIndex(oldItem);
}
}
return list.setItemAt(item, listIndex);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addItem(item:Object):void
{
if (localIndex)
addItemAt(item, localIndex.length);
else
addItemAt(item, length);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addItemAt(item:Object, index:int):void
{
if (index < 0 || !list || index > length)
{
var message:String = resourceManager.getString(
"collections", "outOfBounds", [ index ]);
throw new RangeError(message);
}
var listIndex:int = index;
// if we're sorted addItemAt is meaningless, just add to the end
if (localIndex && sort)
{
listIndex = list.length;
}
else if (localIndex && filterFunction != null)
{
// if end of filtered list, put at end of source list
if (listIndex == localIndex.length)
listIndex = list.length;
// if somewhere in filtered list, find it and insert before it
// or at beginning
else
listIndex = list.getItemIndex(localIndex[index]);
}
// List is sorted or filtered but refresh has not been called
// Just add to end of list
else if (localIndex)
{
listIndex = list.length;
}
list.addItemAt(item, listIndex);
}
/**
* Adds a list of items to the current list, placing them at the end of
* the list in the order they are passed.
*
* @param addList IList The list of items to add to the current list
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addAll(addList:IList):void
{
if (localIndex)
addAllAt(addList, localIndex.length);
else
addAllAt(addList, length);
}
/**
* Adds a list of items to the current list, placing them at the position
* index passed in to the function. The items are placed at the index location
* and placed in the order they are recieved.
*
* @param addList IList The list of items to add to the current list
* @param index The location of the current list to place the new items.
* @throws RangeError if index is less than 0 or greater than the length of the list.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addAllAt(addList:IList, index:int):void
{
if (index < 0 || index > this.length)
{
var message:String = resourceManager.getString(
"collections", "outOfBounds", [ index ]);
throw new RangeError(message);
}
var length:int = addList.length;
for (var i:int=0; i < length; i++)
{
var insertIndex:int = i + index;
// incremental index may be out of bounds because of filtering,
// so add this item to the end.
if (insertIndex > this.length)
insertIndex = this.length;
this.addItemAt(addList.getItemAt(i), insertIndex);
}
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function getItemIndex(item:Object):int
{
var i:int;
if (localIndex && filterFunction != null)
{
var len:int = localIndex.length;
for (i = 0; i < len; i++)
{
if (localIndex[i] == item)
return i;
}
return -1;
}
else if (localIndex && sort)
{
var startIndex:int = findItem(item, Sort.FIRST_INDEX_MODE);
if (startIndex == -1)
return -1;
var endIndex:int = findItem(item, Sort.LAST_INDEX_MODE);
for (i = startIndex; i <= endIndex; i++)
{
if (localIndex[i] == item)
return i;
}
return -1;
}
// List is sorted or filtered but refresh has not been called
else if (localIndex)
{
len = localIndex.length;
for (i = 0; i < len; i++)
{
if (localIndex[i] == item)
return i;
}
return -1;
}
// fallback
return list.getItemIndex(item);
}
/**
* @inheritDoc
*/
mx_internal function getLocalItemIndex(item:Object):int
{
var i:int;
var len:int = localIndex.length;
for (i = 0; i < len; i++)
{
if (localIndex[i] == item)
return i;
}
return -1;
}
/**
* @private
*/
private function getFilteredItemIndex(item:Object):int
{
// loc is wrong
// the intent of this function is to find where this new item
// should be in the filtered list, by looking at the main list
// for it's neighbor that is also in this filtered list
// and trying to insert item after that neighbor in the insert locao filtered list
// 1st get the position in the original list
var loc:int = list.getItemIndex(item);
// something gone wrong and list is not filtered so just return loc to stop RTE
if (filterFunction == null)
return loc;
// if it's 0 then item must be also the first in the filtered list
if (loc == 0)
return 0;
// scan backwards for an item that also in the filtered list
for (var i:int = loc - 1; i >= 0; i--)
{
var prevItem:Object = list.getItemAt(i);
if (filterFunction(prevItem))
{
var len:int = localIndex.length;
// get the index of the item in the filtered set
// for (var j:int = 0; j < len; j++)
for (var j:int = 0; j < len; j++)
{
if (localIndex[j] == prevItem)
return j + 1;
}
}
}
// turns out that there are no neighbors of item in the filtered
// list, so item is the 1st item
return 0;
}
/**
* 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
{
if ("removeItem" in list)
return list["removeItem"](item);
return false;
}
/**
* @inheritDoc
*
* @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);
}
var listIndex:int = index;
if (localIndex)
{
var oldItem:Object = localIndex[index];
listIndex = list.getItemIndex(oldItem);
}
return list.removeItemAt(listIndex);
}
/**
* Remove all items from the list.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeAll():void
{
var len:int = length;
if (len > 0)
{
if (localIndex && filterFunction != null)
{
len = localIndex.length;
for (var i:int = len - 1; i >= 0; i--)
{
removeItemAt(i);
}
}
else
{
localIndex = null;
list.removeAll();
}
}
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function toArray():Array
{
var ret:Array;
if (localIndex)
ret = localIndex.concat();
else
ret = list.toArray();
return ret;
}
/**
* Prints the contents of this view to a string and returns it.
*
* @return The contents of this view, in string form.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function toString():String
{
if (localIndex)
{
return ObjectUtil.toString(localIndex);
}
else
{
if (list && Object(list).toString)
return Object(list).toString();
else
return getQualifiedClassName(this);
}
}
//--------------------------------------------------------------------------
//
// Proxy methods
//
//--------------------------------------------------------------------------
/**
* @private
* Attempts to call getItemAt(), converting the property name into an int.
*/
override flash_proxy function getProperty(name:*):*
{
if (name is QName)
name = name.localName;
try
{
var n:Number = parseInt(String(name));
}
catch(e:Error) // localName was not a number
{
}
if (isNaN(n))
{
var message:String = resourceManager.getString(
"collections", "unknownProperty", [ name ]);
throw new Error(message);
}
else
{
// If caller passed in a number such as 5.5, it will be floored.
return getItemAt(int(n));
}
}
/**
* @private
* Attempts to call setItemAt(), converting the property name into an int.
*/
override flash_proxy function setProperty(name:*, value:*):void
{
if (name is QName)
name = name.localName;
try
{
var n:Number = parseInt(String(name));
}
catch(e:Error) // localName was not a number
{
}
if (isNaN(n))
{
var message:String = resourceManager.getString(
"collections", "unknownProperty", [ name ]);
throw new Error(message);
}
else
{
// If caller passed in a number such as 5.5, it will be floored.
setItemAt(value, int(n));
}
}
/**
* @private
* This is an internal function.
* The VM will call this method for code like <code>"foo" in bar</code>
*
* @param name The property name that should be tested for existence.
*/
override flash_proxy function hasProperty(name:*):Boolean
{
if (name is QName)
name = name.localName;
var index:int = -1;
try
{
// If caller passed in a number such as 5.5, it will be floored.
var n:Number = parseInt(String(name));
if (!isNaN(n))
index = int(n);
}
catch(e:Error) // localName was not a number
{
}
if (index == -1)
return false;
return index >= 0 && index < length;
}
/**
* @private
*/
override flash_proxy function nextNameIndex(index:int):int
{
return index < length ? index + 1 : 0;
}
/**
* @private
*/
override flash_proxy function nextName(index:int):String
{
return (index - 1).toString();
}
/**
* @private
*/
override flash_proxy function nextValue(index:int):*
{
return getItemAt(index - 1);
}
/**
* @private
* Any methods that can't be found on this class shouldn't be called,
* so return null
*/
override flash_proxy function callProperty(name:*, ... rest):*
{
return null;
}
//--------------------------------------------------------------------------
//
// EventDispatcher methods
//
//--------------------------------------------------------------------------
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addEventListener(eventType:String,
listener:Function,
useCapture:Boolean = false,
priority:int = 0,
useWeakReference:Boolean = false):void
{
eventDispatcher.addEventListener(eventType, listener, useCapture,
priority, useWeakReference);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeEventListener(eventType:String,
listener:Function,
useCapture:Boolean = false):void
{
eventDispatcher.removeEventListener(eventType, listener, useCapture);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function dispatchEvent(event:Event):Boolean
{
return eventDispatcher.dispatchEvent(event);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function hasEventListener(eventType:String):Boolean
{
return eventDispatcher.hasEventListener(eventType);
}
/**
* @inheritDoc
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function willTrigger(eventType:String):Boolean
{
return eventDispatcher.willTrigger(eventType);
}
//--------------------------------------------------------------------------
//
// Internal methods
//
//--------------------------------------------------------------------------
/**
* Take the item and insert it into the view. If we don't have a sort
* use the sourceLocation. Dispatch the CollectionEvent with kind ADD
* if dispatch is true.
*
* @param items the items to add into the view
* @param sourceLocation the location within the list where the items were added
* @param dispatch true if the view should dispatch a corresponding
* CollectionEvent with kind ADD (default is true)
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function addItemsToView(items:Array, sourceLocation:int,
dispatch:Boolean = true):int
{
var addedItems:Array = localIndex ? [] : items;
var addLocation:int = sourceLocation;
var firstOne:Boolean = true;
if (localIndex)
{
var loc:int = sourceLocation;
var length:int = items.length;
for (var i:int = 0; i < length; i++)
{
var item:Object = items[i];
if (filterFunction != null)
{
if (filterFunction(item))
{
if (sort)
loc = findItem(item, Sort.ANY_INDEX_MODE, true);
else
loc = getFilteredItemIndex(item);
if (firstOne)
{
addLocation = loc;
firstOne = false;
}
}
else
{
loc = -1;
addLocation = -1;
}
}
else if (sort)
{
loc = findItem(item, Sort.ANY_INDEX_MODE, true);
if (firstOne)
{
addLocation = loc;
firstOne = false;
}
}
else
// List is sorted or filtered but refresh has not been called
// Just add to end of list
{
loc = localIndex.length;
addLocation = loc;
}
if (loc != -1)
{
localIndex.splice(loc++, 0, item);
addedItems.push(item);
}
}
}
if (sort && sort.unique && sort.compareFunction(item, localIndex[loc]) == 0)
{
// We cause all adds to fail here, not just the one.
var message:String = resourceManager.getString(
"collections", "incorrectAddition");
throw new CollectionViewError(message);
}
if (localIndex && addedItems.length > 1)
{
addLocation = -1;
}
if (dispatch && addedItems.length > 0)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.ADD;
event.location = addLocation;
event.items = addedItems;
dispatchEvent(event);
}
return addLocation;
}
/**
* Find the item specified using the Sort find mode constants.
* If there is no sort assigned throw an error.
*
* @param values the values object that can be passed into Sort.findItem
* @param mode the mode to pass to Sort.findItem (see Sort)
* @param insertIndex true if it should find the insertion point
* @return the index where the item is located, -1 if not found or SortError
* caught
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
mx_internal function findItem(values:Object, mode:String, insertIndex:Boolean = false):int
{
if (!sort || !localIndex)
{
var message:String = resourceManager.getString(
"collections", "itemNotFound");
throw new CollectionViewError(message);
}
if (localIndex.length == 0)
return insertIndex ? 0 : -1;
try
{
return sort.findItem(localIndex, values, mode, insertIndex);
}
catch (e:SortError)
{
// usually because the find criteria is not compatible with the sort.
}
return -1;
}
/**
* Create a bookmark for this view. This method is called by
* ListCollectionViewCursor.
*
* @param index the index to bookmark
* @return a new bookmark instance
* @throws a CollectionViewError if the index is out of bounds
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
mx_internal function getBookmark(index:int):ListCollectionViewBookmark
{
if (index < 0 || index > length)
{
var message:String = resourceManager.getString(
"collections", "invalidIndex", [ index ]);
throw new CollectionViewError(message);
}
var value:Object;
try
{
value = getItemAt(index);
}
catch(e:Error)
{
}
return new ListCollectionViewBookmark(value,
this,
revision,
index);
}
/**
* Given a bookmark find the location for the value. If the
* view has been modified since the bookmark was created attempt
* to relocate the item. If the bookmark represents an item
* that is no longer in the view (removed or filtered out) return
* -1.
*
* @param bookmark the bookmark to locate
* @return the new location of the bookmark, -1 if not in the view anymore
* @throws CollectionViewError if the bookmark is invalid
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
mx_internal function getBookmarkIndex(bookmark:CursorBookmark):int
{
if (!(bookmark is ListCollectionViewBookmark)
|| ListCollectionViewBookmark(bookmark).view != this)
{
var message:String = resourceManager.getString(
"collections", "bookmarkNotFound");
throw new CollectionViewError(message);
}
var bm:ListCollectionViewBookmark = ListCollectionViewBookmark(bookmark);
if (bm.viewRevision != revision)
{
// getItemAt has a side-effect of throwing an exception if the index is out-
// of-range, here we are checking to see if the index falls with-in the range
// and only then calling getItemAt.
if (bm.index < 0 || bm.index >= length || getItemAt(bm.index) != bm.value)
{
try
{
bm.index = getItemIndex(bm.value);
}
catch (e:SortError)
{
bm.index = getLocalItemIndex(bm.value);
}
}
bm.viewRevision = revision;
}
return bm.index;
}
/**
* The view is a listener of CollectionEvents on its underlying IList
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function listChangeHandler(event:CollectionEvent):void
{
if (autoUpdateCounter > 0)
{
if (!pendingUpdates)
{
pendingUpdates = [];
}
pendingUpdates.push(event);
}
else
{
switch (event.kind)
{
case CollectionEventKind.ADD:
addItemsToView(event.items, event.location);
break;
case CollectionEventKind.MOVE:
var n:int = event.items.length;
for (var i:int = 0; i < n; i++)
moveItemInView(event.items[i]);
break;
case CollectionEventKind.RESET:
reset();
break;
case CollectionEventKind.REMOVE:
removeItemsFromView(event.items, event.location);
break;
case CollectionEventKind.REPLACE:
replaceItemsInView(event.items, event.location);
break;
case CollectionEventKind.UPDATE:
handlePropertyChangeEvents(event.items);
break;
default:
dispatchEvent(event);
} // switch
}
}
/**
* Given a set of <code>PropertyChangeEvent</code>s go through and update the view.
* This is currently not optimized.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function handlePropertyChangeEvents(events:Array):void
{
var eventItems:Array = events;
if (sort || filterFunction != null)
{
//go through the events and find all the individual objects
//that have been updated
//then for each one determine whether we should move it or
//just fire an update event
var updatedItems:Array = [];
var updateEntry:Object;
var i:int;
for (i = 0; i < events.length; i++)
{
var updateInfo:PropertyChangeEvent = events[i];
var item:Object;
var defaultMove:Boolean;
if (updateInfo.target)
{
item = updateInfo.target;
//if the target != source that means the update
//happened to some subprop of the item in the collection
//if we have a custom comparator this will affect
//the sort so for now say we should move but
//maybe we could optimize further
defaultMove = updateInfo.target != updateInfo.source;
}
else
{
item = updateInfo.source;
defaultMove = false;
}
//see if the item is already in the list
var j:int = 0;
for (; j < updatedItems.length; j++)
{
if (updatedItems[j].item == item)
{
// even if it is, if a different property changed, track that too.
var evts:Array = updatedItems[j].events as Array;
var l:int = evts.length;
for (var k:int = 0; k < l; k++)
{
if (evts[k].property != updateInfo.property)
{
evts.push(updateInfo);
break;
}
// we could also merge events for changes to the same
// property but that's hopefully low probability
// and leads to questions of event order.
}
break;
}
}
if (j < updatedItems.length)
{
updateEntry = updatedItems[j];
}
else
{
updateEntry = {item: item, move: defaultMove, events: [updateInfo],
entireObjectChanged: updateInfo.property == null, oldItem: updateInfo.property == null ? updateInfo.oldValue : null};
updatedItems.push(updateEntry);
}
//if we've already set replace don't unset it
//if there's a filterFunction need to go through replace
//if there's no property specified for the sort we'll need
//to assume we have to replace
//if there is a property see if it affects the sort
updateEntry.move =
updateEntry.move
|| filterFunction != null
|| updateEntry.entireObjectChanged
|| (sort && sort.propertyAffectsSort(String(updateInfo.property)));
}
// go through the items and move and send move events for ones that moved
// and build the list of remaining items we need to send UPDATE events for
eventItems = [];
for (i = 0; i < updatedItems.length; i++)
{
updateEntry = updatedItems[i];
if (updateEntry.move)
{
if(updateEntry.entireObjectChanged)
{
removeItemsFromView([updateEntry.oldItem], -1, true);
}
moveItemInView(updateEntry.item, updateEntry.item, eventItems);
}
else
{
eventItems.push(updateEntry.item);
}
}
// now go through the updated items and add all events for all
// properties that changed in that item (except for those items
// we moved
var temp:Array = [];
for (var ctr:int = 0; ctr < eventItems.length; ctr++)
for (var ctr1:int = 0; ctr1 < updatedItems.length; ctr1++)
if (eventItems[ctr] == updatedItems[ctr1].item)
{
temp = temp.concat(updatedItems[ctr1].events);
}
eventItems = temp;
}
if (eventItems.length > 0)
{
var updateEvent:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
updateEvent.kind = CollectionEventKind.UPDATE;
updateEvent.items = eventItems;
dispatchEvent(updateEvent);
}
}
/**
* When enableAutoUpdates pushes autoUpdateCounter back down to 0
* this method will execute to consolidate the pending update
* events or turn it into a massive refresh().
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function handlePendingUpdates():void
{
if (pendingUpdates)
{
var pu:Array = pendingUpdates;
pendingUpdates = null;
// Could further optimize to consolidate various events
// and make a decision if there are too many updates
// and we should just refresh.
var singleUpdateEvent:CollectionEvent;
for (var i:int = 0; i < pu.length; i++)
{
var event:CollectionEvent = pu[i];
if (event.kind == CollectionEventKind.UPDATE)
{
if (!singleUpdateEvent)
{
singleUpdateEvent = event;
}
else
{
for (var j:int = 0; j < event.items.length; j++)
{
singleUpdateEvent.items.push(event.items[j]);
}
}
}
else
{
listChangeHandler(event);
}
}
if (singleUpdateEvent)
{
listChangeHandler(singleUpdateEvent);
}
}
}
private function internalRefresh(dispatch:Boolean):Boolean
{
if (sort || filterFunction != null)
{
try
{
populateLocalIndex();
}
catch(pending:ItemPendingError)
{
pending.addResponder(new ItemResponder(
function(data:Object, token:Object = null):void
{
internalRefresh(dispatch);
},
function(info:Object, token:Object = null):void
{
//no-op
}));
return false;
}
if (filterFunction != null)
{
var tmp:Array = [];
var len:int = localIndex.length;
for (var i:int = 0; i < len; i++)
{
var item:Object = localIndex[i];
if (filterFunction(item))
{
tmp.push(item);
}
}
localIndex = tmp;
}
if (sort)
{
sort.sort(localIndex);
dispatch = true;
}
}
else if (localIndex)
{
localIndex = null;
}
revision++;
pendingUpdates = null;
if (dispatch)
{
var refreshEvent:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
refreshEvent.kind = CollectionEventKind.REFRESH;
dispatchEvent(refreshEvent);
}
return true;
}
/**
* Remove the old value from the view and replace it with the value
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function moveItemInView(item:Object, dispatch:Boolean = true, updateEventItems:Array = null):void
{
if (localIndex)
{
//we're guaranteed that removeItemsFromView isn't going
//to work here because the item has probably
//already been updated so getItemIndex is going to fail
//so we'll just do a linear search and find it if it's here
var removeLocation:int = -1;
for (var i:int = 0; i < localIndex.length; i++)
{
if (localIndex[i] == item)
{
removeLocation = i;
break;
}
}
if (removeLocation > -1)
{
localIndex.splice(removeLocation, 1);
}
var addLocation:int = addItemsToView([item], removeLocation, false);
if (dispatch)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.items.push(item);
if (updateEventItems && addLocation == removeLocation && addLocation > -1)
{
updateEventItems.push(item);
return;
}
if (addLocation > -1 && removeLocation > -1)
{
event.kind = CollectionEventKind.MOVE;
event.location = addLocation;
event.oldLocation = removeLocation;
}
else if (addLocation > -1)
{
event.kind = CollectionEventKind.ADD;
event.location = addLocation;
}
else if (removeLocation > -1)
{
event.kind = CollectionEventKind.REMOVE;
event.location = removeLocation;
}
else
{
dispatch = false;
}
if (dispatch)
{
dispatchEvent(event);
}
}
}
}
/**
* Copy all of the data from the source list into the local index.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function populateLocalIndex():void
{
if (list)
{
localIndex = list.toArray();
}
else
{
localIndex = [];
}
}
/**
* Take the item and remove it from the view. If we don't have a sort
* use the sourceLocation. Dispatch the CollectionEvent with kind REMOVE
* if dispatch is true.
*
* @param items the items to remove from the view
* @param sourceLocation the location within the list where the item was removed
* @param dispatch true if the view should dispatch a corresponding
* CollectionEvent with kind REMOVE (default is true)
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function removeItemsFromView(items:Array, sourceLocation:int, dispatch:Boolean = true):void
{
var removedItems:Array = localIndex ? [] : items;
var removeLocation:int = sourceLocation;
if (localIndex)
{
for (var i:int = 0; i < items.length; i++)
{
var item:Object = items[i];
var loc:int = getItemIndex(item);
if (loc > -1)
{
localIndex.splice(loc, 1);
removedItems.push(item);
removeLocation = loc;
}
}
}
if (dispatch && removedItems.length > 0)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.REMOVE;
event.location = (!localIndex || removedItems.length == 1)
? removeLocation
: -1;
event.items = removedItems;
dispatchEvent(event);
}
}
/**
* Items is an array of PropertyChangeEvents so replace the oldValues with the new
* newValues. Start at the location specified and move forward, it's unlikely
* that the length of items is > 1.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
private function replaceItemsInView(changeEvents:Array,
location:int,
dispatch:Boolean = true):void
{
if (localIndex)
{
var len:int = changeEvents.length;
var oldItems:Array = [];
var newItems:Array = [];
for (var i:int = 0; i < len; i++)
{
var propertyEvent:PropertyChangeEvent = changeEvents[i];
oldItems.push(propertyEvent.oldValue);
newItems.push(propertyEvent.newValue);
}
removeItemsFromView(oldItems, location, dispatch);
addItemsToView(newItems, location, dispatch);
}
else
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.REPLACE;
event.location = location;
event.items = changeEvents;
dispatchEvent(event);
}
}
/**
* @private
* When the source list is replaced, reset.
*/
mx_internal function reset():void
{
internalRefresh(false);
if (dispatchResetEvent)
{
var event:CollectionEvent =
new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.RESET;
dispatchEvent(event);
}
}
public function get complexFieldWatcher():ComplexFieldChangeWatcher
{
return _complexFieldWatcher;
}
public function set complexFieldWatcher(value:ComplexFieldChangeWatcher):void
{
if(_complexFieldWatcher != value)
{
stopWatchingForComplexFieldsChanges();
_complexFieldWatcher = value;
if(_complexFieldWatcher)
_complexFieldWatcher.mx_internal::list = this;
startWatchingForComplexFieldsChanges();
}
}
private function startWatchingForComplexFieldsChanges():void
{
if(complexFieldWatcher && sort && sort.fields)
{
_complexFieldWatcher.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onComplexFieldValueChanged, false, 0, true);
_complexFieldWatcher.startWatchingForComplexFieldChanges();
}
}
private function stopWatchingForComplexFieldsChanges():void
{
if(complexFieldWatcher)
{
_complexFieldWatcher.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, onComplexFieldValueChanged);
_complexFieldWatcher.stopWatchingForComplexFieldChanges();
}
}
private function onComplexFieldValueChanged(changeEvent:PropertyChangeEvent):void
{
if(sort)
{
moveItemInView(changeEvent.source);
}
}
}
}
import flash.events.Event;
import flash.events.EventDispatcher;
import mx.collections.*;
import mx.collections.errors.*;
import mx.core.mx_internal;
import mx.events.*;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
use namespace mx_internal;
/**
* Dispatched whenever the cursor position is updated.
*
* @eventType mx.events.FlexEvent.CURSOR_UPDATE
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
[Event(name="cursorUpdate", type="mx.events.FlexEvent")]
[ResourceBundle("collections")]
/**
* @private
* The internal implementation of cursor for the ListCollectionView.
*/
class ListCollectionViewCursor extends EventDispatcher implements IViewCursor
{
//--------------------------------------------------------------------------
//
// Class constants
//
//--------------------------------------------------------------------------
/**
* @private
*/
private static const BEFORE_FIRST_INDEX:int = -1;
/**
* @private
*/
private static const AFTER_LAST_INDEX:int = -2;
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
/**
* Constructor.
*
* <p>Creates the cursor for the view.</p>
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function ListCollectionViewCursor(view:ListCollectionView)
{
super();
_view = view;
_view.addEventListener(CollectionEvent.COLLECTION_CHANGE, collectionEventHandler, false, 0, true);
currentIndex = view.length > 0 ? 0 : AFTER_LAST_INDEX;
if (currentIndex == 0)
{
try
{
setCurrent(view.getItemAt(0), false);
}
catch(e:ItemPendingError)
{
currentIndex = BEFORE_FIRST_INDEX;
setCurrent(null, false);
}
}
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
*/
private var _view:ListCollectionView;
/**
* @private
*/
private var currentIndex:int;
/**
* @private
*/
private var currentValue:Object;
/**
* @private
*/
private var invalid:Boolean;
/**
* @private
* Used for accessing localized Error messages.
*/
private var resourceManager:IResourceManager =
ResourceManager.getInstance();
//--------------------------------------------------------------------------
//
// Properties
//
//--------------------------------------------------------------------------
/**
* Get a reference to the view that this cursor is associated with.
* @return the associated <code>ICollectionView</code>
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get view():ICollectionView
{
checkValid();
return _view;
}
[Bindable("cursorUpdate")]
/**
* Provides access the object at the current location referenced by
* this cursor within the source collection.
* If the cursor is beyond the ends of the collection (beforeFirst,
* afterLast) this will return <code>null</code>.
*
* @see mx.collections.IViewCursor#moveNext
* @see mx.collections.IViewCursor#movePrevious
* @see mx.collections.IViewCursor#seek
* @see mx.collections.IViewCursor#beforeFirst
* @see mx.collections.IViewCursor#afterLast
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get current():Object
{
checkValid();
return currentValue;
}
[Bindable("cursorUpdate")]
/**
* Provides access to the bookmark of the item returned by the
* <code>current</code> property.
* The bookmark can be used to move the cursor to a previously visited
* item, or one relative to it (see the <code>seek()</code> method for
* more information).
*
* @see mx.collections.IViewCursor#current
* @see mx.collections.IViewCursor#seek
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get bookmark():CursorBookmark
{
checkValid();
if (view.length == 0 || beforeFirst) return CursorBookmark.FIRST;
else if (afterLast) return CursorBookmark.LAST;
//if currentIndex > view.length this is a bug in cursor and i want the
//exception thrown to track it down
else return ListCollectionView(view).getBookmark(currentIndex);
}
[Bindable("cursorUpdate")]
/**
* true if the current is sitting before the first item in the view.
* If the ICollectionView is empty (length == 0) this will always
* be true.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get beforeFirst():Boolean
{
checkValid();
return currentIndex == BEFORE_FIRST_INDEX || view.length == 0;
}
[Bindable("cursorUpdate")]
/**
* true if the cursor is sitting after the last item in the view.
* If the ICollectionView is empty (length == 0) this will always
* be true.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get afterLast():Boolean
{
checkValid();
return currentIndex == AFTER_LAST_INDEX || view.length == 0;
}
/**
* Finds the item with the specified properties within the
* collection and positions the cursor on that item.
* If the item can not be found no change to the current location will be
* made.
* <code>findAny()</code> can only be called on sorted views, if the view
* isn't sorted, or items in the view do not contain properties used
* to compute the sort order, a <code>CursorError</code> will be thrown.
* <p>
* If the associated collection is remote, and not all of the items have
* been cached locally this method will begin an asynchronous fetch from the
* remote collection, or if one is already in progress wait for it to
* complete before making another fetch request.
* If multiple items can match the search criteria then the item found is
* non-deterministic.
* If it is important to find the first or last occurrence of an item in a
* non-unique index use the <code>findFirst()</code> or
* <code>findLast()</code>.
* The values specified must be configured as name-value pairs, as in an
* associative array (or the actual object to search for).
* The values of the names specified must match those properties specified in
* the sort. for example
* If properties "x", "y", and "z" are the in the current index, the values
* specified should be {x:x-value, y:y-value,z:z-value}.
* When all of the data is local this method will return <code>true</code> if
* the item can be found and false otherwise.
* If the data is not local and an asynchronous operation must be performed,
* an <code>ItemPendingError</code> will be thrown.
*
* @see mx.collections.IViewCursor#findFirst
* @see mx.collections.IViewCursor#findLast
* @see mx.collections.errors.ItemPendingError
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function findAny(values:Object):Boolean
{
checkValid();
var lcView:ListCollectionView = ListCollectionView(view);
var index:int;
try
{
index = lcView.findItem(values, Sort.ANY_INDEX_MODE);
}
catch(e:SortError)
{
//this is because the find critieria is not compatible with the
//sort
throw new CursorError(e.message);
}
if (index > -1)
{
currentIndex = index;
setCurrent(lcView.getItemAt(currentIndex));
}
return index > -1;
}
/**
* Finds the first item with the specified properties
* within the collection and positions the cursor on that item.
* If the item can not be found no change to the current location will be
* made.
* <code>findFirst()</code> can only be called on sorted views, if the view
* isn't sorted, or items in the view do not contain properties used
* to compute the sort order, a <code>CursorError</code> will be thrown.
* <p>
* If the associated collection is remote, and not all of the items have been
* cached locally this method will begin an asynchronous fetch from the
* remote collection, or if one is already in progress wait for it to
* complete before making another fetch request.
* If it is not important to find the first occurrence of an item in a
* non-unique index use <code>findAny()</code> as it may be a little faster.
* The values specified must be configured as name-value pairs, as in an
* associative array (or the actual object to search for).
* The values of the names specified must match those properties specified in
* the sort. for example If properties "x", "y", and "z" are the in the current
* index, the values specified should be {x:x-value, y:y-value,z:z-value}.
* When all of the data is local this method will
* return <code>true</code> if the item can be found and false otherwise.
* If the data is not local and an asynchronous operation must be performed,
* an <code>ItemPendingError</code> will be thrown.
*
* @see mx.collections.IViewCursor#findAny
* @see mx.collections.IViewCursor#findLast
* @see mx.collections.errors.ItemPendingError
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function findFirst(values:Object):Boolean
{
checkValid();
var lcView:ListCollectionView = ListCollectionView(view);
var index:int;
try
{
index = lcView.findItem(values, Sort.FIRST_INDEX_MODE);
}
catch(sortError:SortError)
{
//this is because the find critieria is not compatible with the
//sort
throw new CursorError(sortError.message);
}
if (index > -1)
{
currentIndex = index;
setCurrent(lcView.getItemAt(currentIndex));
}
return index > -1;
}
/**
* Finds the last item with the specified properties
* within the collection and positions the cursor on that item.
* If the item can not be found no change to the current location will be
* made.
* <code>findLast()</code> can only be called on sorted views, if the view
* isn't sorted, or items in the view do not contain properties used
* to compute the sort order, a <code>CursorError</code> will be thrown.
* <p>
* If the associated collection is remote, and not all of the items have been
* cached locally this method will begin an asynchronous fetch from the
* remote collection, or if one is already in progress wait for it to
* complete before making another fetch request.
* If it is not important to find the last occurrence of an item in a
* non-unique index use <code>findAny()</code> as it may be a little faster.
* The values specified must be configured as name-value pairs, as in an
* associative array (or the actual object to search for).
* The values of the names specified must match those properties specified in
* the sort. for example If properties "x", "y", and "z" are the in the current
* index, the values specified should be {x:x-value, y:y-value,z:z-value}.
* When all of the data is local this method will
* return <code>true</code> if the item can be found and false otherwise.
* If the data is not local and an asynchronous operation must be performed,
* an <code>ItemPendingError</code> will be thrown.
*
* @see mx.collections.IViewCursor#findAny
* @see mx.collections.IViewCursor#findFirst
* @see mx.collections.errors.ItemPendingError
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function findLast(values:Object):Boolean
{
checkValid();
var lcView:ListCollectionView = ListCollectionView(view);
var index:int;
try
{
index = lcView.findItem(values, Sort.LAST_INDEX_MODE);
}
catch(sortError:SortError)
{
//this is because the find critieria is not compatible with the
//sort
throw new CursorError(sortError.message);
}
if (index > -1)
{
currentIndex = index;
setCurrent(lcView.getItemAt(currentIndex));
}
return index > -1;
}
/**
* Insert the specified item before the cursor's current position.
* If the cursor is <code>afterLast</code> the insertion
* will happen at the end of the View. If the cursor is
* <code>beforeFirst</code> on a non-empty view an error will be thrown.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function insert(item:Object):void
{
var insertIndex:int;
if (afterLast)
{
insertIndex = view.length;
}
else if (beforeFirst)
{
if (view.length > 0)
{
var message:String = resourceManager.getString(
"collections", "invalidInsert");
throw new CursorError(message);
}
else
{
insertIndex = 0;
}
}
else
{
insertIndex = currentIndex;
}
ListCollectionView(view).addItemAt(item, insertIndex);
}
/**
* Moves the cursor to the next item within the collection. On success
* the <code>current</code> property will be updated to reference the object at this
* new location. Returns true if current is valid, false if not (afterLast).
* If the data is not local and an asynchronous operation must be performed, an
* <code>ItemPendingError</code> will be thrown. See the ItemPendingError docs
* as well as the collections documentation for more information on using the
* ItemPendingError.
*
* @return true if still in the list, false if current is now afterLast
*
* @see mx.collections.IViewCursor#current
* @see mx.collections.IViewCursor#movePrevious
* @see mx.collections.errors.ItemPendingError
* @example
* <pre>
* var myArrayCollection:ICollectionView = new ArrayCollection(["Bobby", "Mark", "Trevor", "Jacey", "Tyler"]);
* var cursor:IViewCursor = myArrayCollection.createCursor();
* while (!cursor.afterLast)
* {
* trace(cursor.current);
* cursor.moveNext();
* }
* </pre>
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function moveNext():Boolean
{
//the afterLast getter checks validity and also checks length > 0
if (afterLast)
{
return false;
}
// we can't set the index until we know that we can move there first.
var tempIndex:int = beforeFirst ? 0 : currentIndex + 1;
if (tempIndex >= view.length)
{
tempIndex = AFTER_LAST_INDEX;
setCurrent(null);
}
else
{
setCurrent(ListCollectionView(view).getItemAt(tempIndex));
}
currentIndex = tempIndex;
return !afterLast;
}
/**
* Moves the cursor to the previous item within the collection. On success
* the <code>current</code> property will be updated to reference the object at this
* new location. Returns true if current is valid, false if not (beforeFirst).
* If the data is not local and an asynchronous operation must be performed, an
* <code>ItemPendingError</code> will be thrown. See the ItemPendingError docs
* as well as the collections documentation for more information on using the
* ItemPendingError.
*
* @return true if still in the list, false if current is now beforeFirst
*
* @see mx.collections.IViewCursor#current
* @see mx.collections.IViewCursor#moveNext
* @see mx.collections.errors.ItemPendingError
* @example
* <pre>
* var myArrayCollection:ICollectionView = new ArrayCollection(["Bobby", "Mark", "Trevor", "Jacey", "Tyler"]);
* var cursor:ICursor = myArrayCollection.createCursor();
* cursor.seek(CursorBookmark.last);
* while (!cursor.beforeFirst)
* {
* trace(current);
* cursor.movePrevious();
* }
* </pre>
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function movePrevious():Boolean
{
//the afterLast getter checks validity and also checks length > 0
if (beforeFirst)
{
return false;
}
// we can't set the index until we know that we can move there first
var tempIndex:int = afterLast ? view.length - 1 : currentIndex - 1;
if (tempIndex == -1)
{
tempIndex = BEFORE_FIRST_INDEX;
setCurrent(null);
}
else
{
setCurrent(ListCollectionView(view).getItemAt(tempIndex));
}
currentIndex = tempIndex;
return !beforeFirst;
}
/**
* Remove the current item and return it. If the cursor is
* <code>beforeFirst</code> or <code>afterLast</code> throw a
* CursorError.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function remove():Object
{
if (beforeFirst || afterLast)
{
var message:String = resourceManager.getString(
"collections", "invalidRemove");
throw new CursorError(message);
}
var oldIndex:int = currentIndex;
currentIndex++;
if (currentIndex >= view.length)
{
currentIndex = AFTER_LAST_INDEX;
setCurrent(null);
}
else
{
try
{
setCurrent(ListCollectionView(view).getItemAt(currentIndex));
}
catch(e:ItemPendingError)
{
setCurrent(null, false);
ListCollectionView(view).removeItemAt(oldIndex);
throw e;
}
}
var removed:Object = ListCollectionView(view).removeItemAt(oldIndex);
return removed;
}
/**
* Moves the cursor to a location at an offset from the specified
* bookmark.
* The offset can be negative in which case the cursor is positioned an
* offset number of items prior to the specified bookmark.
* If the associated collection is remote, and not all of the items have been
* cached locally this method will begin an asynchronous fetch from the
* remote collection.
*
* If the data is not local and an asynchronous operation must be performed, an
* <code>ItemPendingError</code> will be thrown. See the ItemPendingError docs
* as well as the collections documentation for more information on using the
* ItemPendingError.
*
*
* @param bookmark <code>CursorBookmark</code> reference to marker information that
* allows repositioning to a specific location.
* In addition to supplying a value returned from the <code>bookmark</code>
* property, there are three constant bookmark values that can be
* specified:
* <ul>
* <li><code>CursorBookmark.FIRST</code> - seek from
* the start (first element) of the collection</li>
* <li><code>CursorBookmark.CURRENT</code> - seek from
* the current position in the collection</li>
* <li><code>CursorBookmark.LAST</code> - seek from the
* end (last element) of the collection</li>
* </ul>
* @param offset indicates how far from the specified bookmark to seek.
* If the specified number is negative the cursor will attempt to
* move prior to the specified bookmark, if the offset specified is
* beyond the end points of the collection the cursor will be
* positioned off the end (beforeFirst or afterLast).
* @param prefetch indicates the intent to iterate in a specific direction once the
* seek operation completes, this reduces the number of required
* network round trips during a seek.
* If the iteration direction is known at the time of the request
* the appropriate amount of data can be returned ahead of the
* request to iterate it.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function seek(bookmark:CursorBookmark, offset:int = 0, prefetch:int = 0):void
{
checkValid();
if (view.length == 0)
{
currentIndex = AFTER_LAST_INDEX;
setCurrent(null, false);
return;
}
var newIndex:int = currentIndex;
if (bookmark == CursorBookmark.FIRST)
{
newIndex = 0;
}
else if (bookmark == CursorBookmark.LAST)
{
newIndex = view.length - 1;
}
else if (bookmark != CursorBookmark.CURRENT)
{
var message:String;
try
{
newIndex = ListCollectionView(view).getBookmarkIndex(bookmark);
if (newIndex < 0)
{
setCurrent(null);
message = resourceManager.getString(
"collections", "bookmarkInvalid");
throw new CursorError(message);
}
}
catch(bmError:CollectionViewError)
{
message = resourceManager.getString(
"collections", "bookmarkInvalid");
throw new CursorError(message);
}
}
newIndex += offset;
var newCurrent:Object = null;
if (newIndex >= view.length)
{
currentIndex = AFTER_LAST_INDEX;
}
else if (newIndex < 0)
{
currentIndex = BEFORE_FIRST_INDEX;
}
else
{
newCurrent = ListCollectionView(view).getItemAt(newIndex, prefetch);
currentIndex = newIndex;
}
setCurrent(newCurrent);
}
//--------------------------------------------------------------------------
//
// Internal methods
//
//--------------------------------------------------------------------------
private function checkValid():void
{
if (invalid)
{
var message:String = resourceManager.getString(
"collections", "invalidCursor");
throw new CursorError(message);
}
}
private function collectionEventHandler(event:CollectionEvent):void
{
switch (event.kind)
{
case CollectionEventKind.ADD:
if (event.location <= currentIndex)
{
currentIndex += event.items.length;
}
break;
case CollectionEventKind.REMOVE:
if (event.location < currentIndex)
{
currentIndex -= event.items.length;
}
else if (event.location == currentIndex)
{
if (currentIndex < view.length)
{
try
{
setCurrent(ListCollectionView(view).getItemAt(currentIndex));
}
catch(error:ItemPendingError)
{
setCurrent(null, false);
}
}
else // currentIndex == view.length
{
//we were removed! is this an error?
//should cursor move to now last item, view.length - 1??
currentIndex = AFTER_LAST_INDEX;
setCurrent(null); //dispatch the updated at least
}
}
break;
case CollectionEventKind.MOVE:
if (event.oldLocation == currentIndex)
{
currentIndex = event.location;
}
else
{
if (event.oldLocation < currentIndex)
{
currentIndex -= event.items.length;
}
if (event.location <= currentIndex)
{
currentIndex += event.items.length;
}
}
break;
case CollectionEventKind.REFRESH:
if (!(beforeFirst || afterLast))
{
try
{
currentIndex = ListCollectionView(view).getItemIndex(currentValue);
}
catch (e:SortError)
{
if (ListCollectionView(view).sort)
{
currentIndex = ListCollectionView(view).getLocalItemIndex(currentValue);
}
}
if (currentIndex == -1)
{
setCurrent(null);
}
}
break;
case CollectionEventKind.REPLACE:
if (event.location == currentIndex)
{
try
{
setCurrent(ListCollectionView(view).getItemAt(currentIndex));
}
catch(error:ItemPendingError)
{
setCurrent(null, false);
}
}
break;
case CollectionEventKind.RESET:
//just move to the beginning
currentIndex = BEFORE_FIRST_INDEX;
setCurrent(null);
break;
}
}
/**
* @private
*/
private function setCurrent(value:Object, dispatch:Boolean = true):void
{
currentValue = value;
if (dispatch)
dispatchEvent(new FlexEvent(FlexEvent.CURSOR_UPDATE));
}
}
/**
* @private
* Encapsulates the positional aspects of a cursor within an ListCollectionView.
* Only the ListCollectionView should construct this.
*/
class ListCollectionViewBookmark extends CursorBookmark
{
mx_internal var index:int;
mx_internal var view:ListCollectionView;
mx_internal var viewRevision:int;
/**
* @private
*/
public function ListCollectionViewBookmark(value:Object,
view:ListCollectionView,
viewRevision:int,
index:int)
{
super(value);
this.view = view;
this.viewRevision = viewRevision;
this.index = index;
}
/**
* Get the approximate index of the item represented by this bookmark
* in its view. If the item has been paged out this may throw an
* ItemPendingError. If the item is not in the current view -1 will be
* returned. This method may also return -1 if index-based location is not
* possible.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
override public function getViewIndex():int
{
return view.getBookmarkIndex(this);
}
}