blob: d6bc2c89f5ef84822489c4a4c7e113cbe25411dd [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 mx.collections.IViewCursor;
import mx.collections.Sort;
import flash.events.Event;
import mx.collections.ICollectionView;
import mx.collections.errors.CollectionViewError;
import mx.collections.errors.CursorError;
import mx.events.CollectionEvent;
import mx.events.CollectionEventKind;
import mx.core.mx_internal;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
import mx.utils.StringUtil;
import mx.events.PropertyChangeEvent;
import flash.utils.Dictionary;
use namespace mx_internal;
[ResourceBundle("collections")]
/**
* @private
* The ModifiedCollectionView class wraps a ListCollectionView object in order
* to provide control over when removed, added, and replaced items are actually
* shown. It is used by list data change effects in order to determine the start
* and end state for effects after changes occur in a collection.
*
* Although it is marked as implementing ICollectionView for interface
* compatibility reasons, many of the properties and methods aren't
* implemented.
*/
public class ModifiedCollectionView implements ICollectionView
{
include "../core/Version.as";
//--------------------------------------------------------------------------
//
// Class constants
//
//--------------------------------------------------------------------------
public static const REMOVED:String = "removed";
public static const ADDED:String = "added";
public static const REPLACED:String = "replaced";
public static const REPLACEMENT:String = "replacement";
//--------------------------------------------------------------------------
//
// Constructor
//
//--------------------------------------------------------------------------
public function ModifiedCollectionView(list:ICollectionView)
{
super();
this.list = list;
}
//--------------------------------------------------------------------------
//
// Private variables
//
//--------------------------------------------------------------------------
/**
* @private
* Used for accessing localized Error messages.
*/
private var resourceManager:IResourceManager =
ResourceManager.getInstance();
/**
* @private
* The underlying collection that this view is wrapping.
*/
private var list:ICollectionView;
/**
* @private
* The number of items that have been added/removed from the
* underlying collection which are being ignored/preserved in this
* collection. Any addition to the underlying collection decrements
* this value, any removal increments it.
*/
private var deltaLength:int = 0;
/**
* @private
* An array of adds/removes from the underlying collection which
* are being ignored/preserved in this wrapper. This elements in
* this array are CollectionModification objects storing changes,
* and are kept in sorted order, according to the location in the
* underlying collection where they occurred.
*/
private var deltas:Array = [];
private var removedItems:Dictionary = new Dictionary(true);
private var addedItems:Dictionary = new Dictionary(true);
private var replacedItems:Dictionary = new Dictionary(true);
private var replacementItems:Dictionary = new Dictionary(true);
//--------------------------------------------------------------------------
//
// ICollectionView Properties
//
//--------------------------------------------------------------------------
/**
* @private
*/
public function get length():int
{
return list.length + (_showPreserved ? deltaLength : 0);
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get filterFunction():Function
{
return null;
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function set filterFunction(value:Function):void
{
}
//--------------------------------------------------------------------------
//
// ICollectionView Methods
//
//--------------------------------------------------------------------------
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function disableAutoUpdate():void
{
}
public function createCursor():IViewCursor
{
var internalCursor:IViewCursor = list.createCursor();
var current:Object = internalCursor.current;
return new ModifiedCollectionViewCursor(this,internalCursor,current);
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function contains(item:Object):Boolean
{
return false;
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get sort():ISort
{
return null;
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function set sort(value:ISort):void
{
}
/**
* Not supported by ModifiedCollectionView
*
* @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
{
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function refresh():Boolean
{
return false;
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function enableAutoUpdate():void
{
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function hasEventListener(type:String):Boolean
{
return false;
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function willTrigger(type:String):Boolean
{
return false;
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addEventListener(type:String, listener:Function, useCapture:Boolean = false, priority:int = 0.0, useWeakReference:Boolean = false):void
{
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeEventListener(type:String, listener:Function, useCapture:Boolean = false):void
{
}
/**
* Not supported by ModifiedCollectionView
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function dispatchEvent(event:Event):Boolean
{
return false;
}
/**
* Create a bookmark for this view. This method is called by
* ModifiedCollectionViewCursor.
*
* @param ModifiedCollectionViewCursor The cursor for which to create the 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(mcvCursor:ModifiedCollectionViewCursor):ModifiedCollectionViewBookmark
{
var index:int = mcvCursor.currentIndex;
if (index < 0 || index > length)
{
var message:String = resourceManager.getString(
"collections", "invalidIndex", [ index ]);
throw new CollectionViewError(message);
}
var value:Object = mcvCursor.current;
return new ModifiedCollectionViewBookmark(value,
this,
0,
index,
mcvCursor.internalCursor.bookmark,
mcvCursor.internalIndex);
}
/**
* 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 ModifiedCollectionViewBookmark)
|| ModifiedCollectionViewBookmark(bookmark).view != this)
{
var message:String = resourceManager.getString(
"collections", "bookmarkNotFound");
throw new CollectionViewError(message);
}
var bm:ModifiedCollectionViewBookmark = ModifiedCollectionViewBookmark(bookmark);
return bm.index;
}
private var itemWrappersByIndex:Array = [];
private var itemWrappersByCollectionMod:Dictionary = new Dictionary(true);
/**
* Given a cursor, and an index, return a wrapped version of the item at
* that index. The item may come either from the underlying collection
* (retrieved through the cursor) or from the annotations stored within
* the modifiedCollectionView.
*
* This method also adjusts the cursor as necessary.
*
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
mx_internal function getWrappedItemUsingCursor(mcvCursor:ModifiedCollectionViewCursor,
newIndex:int):Object
{
// iterate through collection modifications, determining
// how many items have been added/removed before index
var adjustedIndex:int = newIndex;
var item:Object = null;
var cm:CollectionModification = null;
var mod0:CollectionModification;
var isReplacement:Boolean = false;
for (var j:int = 0; j < deltas.length; j++)
{
mod0 = deltas[j];
if (adjustedIndex < mod0.index)
break;
// THIS LOGIC SHOULD BE CONSOLIDATED WITH THE OTHER LOGIC VIA APPROPRIATE REFACTORING
if (mod0.modificationType == CollectionModification.REPLACE)
{
// maybe we need _suppressAdded / _suppressRemoved?
// the semantics of _showAdded/_showRemoved are maybe a bit screwed up.
// In fact, the logic below is relying on mod being initialized in a certain way, etc.
if ((adjustedIndex == mod0.index) && mod0.showOldReplace && _showPreserved)
{
cm = mod0;
break;
}
if ((adjustedIndex == mod0.index + 1) && mod0.showOldReplace && mod0.showNewReplace && _showPreserved)
{
adjustedIndex--;
isReplacement = true;
break;
}
if ((adjustedIndex == mod0.index ) && ((!mod0.showOldReplace && mod0.showNewReplace) || !_showPreserved))
{
isReplacement = true;
break;
}
adjustedIndex -= mod0.modCount;
}
else if (isActive(mod0)) // ignoring is true after addItemAction/removeItemAction...though we probably will just remove from list then
{
if ((adjustedIndex == mod0.index) && mod0.isRemove)
{
cm = mod0;
break;
}
else if (adjustedIndex >= mod0.index)
adjustedIndex -= mod0.modCount;
}
}
if (cm)
item = cm.item;
else
{
// We have to fetch the new item from cursor into the underlying collection
// We'll also adjust the index we maintain for that cursor
mcvCursor.internalCursor.seek(CursorBookmark.CURRENT,adjustedIndex - mcvCursor.internalIndex);
item = mcvCursor.internalCursor.current;
mcvCursor.internalIndex = adjustedIndex;
}
var itemWrapper:Object;
if (mod0 && (adjustedIndex == mod0.index) && (mod0.modificationType == CollectionModification.ADD))
itemWrapper = getUniqueItemWrapper(item,mod0,adjustedIndex)
else
itemWrapper = getUniqueItemWrapper(item,cm,adjustedIndex);
return itemWrapper;
}
//--------------------------------------------------------------------------
//
// Public properties
//
//--------------------------------------------------------------------------
private var _showPreserved:Boolean = false;
/**
* Enables or suppresses the ability of the collection to show
* previous or "preserved" state. If set to false, the
* ModifiedCollectionView will present a view equivalent to the
* current state of the ListCollectionView it is wrapping. If
* set to true, it will present a view of the ListCollectionView
* ignoring any changes that have been integrated into the
* ModifiedCollectionView.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get showPreservedState():Boolean
{
return _showPreserved;
}
public function set showPreservedState(show:Boolean):void
{
_showPreserved = show;
}
//--------------------------------------------------------------------------
//
// Public methods
//
//--------------------------------------------------------------------------
public function getSemantics(itemWrapper:ItemWrapper):String
{
if (removedItems[itemWrapper])
return ModifiedCollectionView.REMOVED;
if (addedItems[itemWrapper])
return ModifiedCollectionView.ADDED;
// TODO (aharui): these won't be quite right yet (won't generate two separate item wrappers for replaced & replacement)
if (replacedItems[itemWrapper])
return ModifiedCollectionView.REPLACED;
if (replacementItems[itemWrapper])
return ModifiedCollectionView.REPLACEMENT;
return null;
}
/**
* Processes a collection event generated by the underlying view. If the
* event is of type ADD, REMOVE, or REPLACE, it is integrated so that
* its effects are ignored if showPreserved is set to true.
*
* @param event A CollectionEvent generated by the ListCollectionView this
* ModifiedCollectionView is wrapping.
*
* @param startItemIndex
*
* @param endItemIndex
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function processCollectionEvent(event:CollectionEvent, startItemIndex:int, endItemIndex:int):void
{
switch (event.kind)
{
case CollectionEventKind.ADD:
integrateAddedElements(event, startItemIndex, endItemIndex);
break;
case CollectionEventKind.REMOVE:
integrateRemovedElements(event, startItemIndex, endItemIndex);
break;
case CollectionEventKind.REPLACE:
integrateReplacedElements(event, startItemIndex, endItemIndex);
break;
}
}
/**
* Stops showing an item that has been removed or replaced
* in the underlying ListCollectionView but which is still
* being shown by the ModifiedCollectionView.
*
* This function is meant to be called by ListBase in response to
* a RemoveItemAction effect.
*
* @param item The item to remove from the collection. This must have
* been removed from the original collection.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function removeItem(itemWrapper:ItemWrapper):void
{
var mod:CollectionModification = removedItems[itemWrapper] as CollectionModification;
if (!mod)
{
mod = replacedItems[itemWrapper] as CollectionModification;
if (mod)
{
delete replacedItems[itemWrapper];
// do this here?? don't think so
// replacementItems[list.getItemAt(mod.index)] = null;
mod.stopShowingReplacedValue();
// need more error checking here
deltaLength--;
// if we're already showing the replacement value, we
// can remove this modification
if (mod.modCount == 0)
removeModification(mod);
}
}
else if (removeModification(mod))
{
delete removedItems[itemWrapper];
deltaLength--;
}
}
/**
* Starts showing an item that has been added to the
* underlying ListCollectionView but which is still
* being ignored by the ModifiedCollectionView.
*
* This function is meant to be called by ListBase in response to
* a AddItemAction effect.
*
* @param item The item to start showing in the collection. This must
* have been added to the original collection.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function addItem(itemWrapper:ItemWrapper):void
{
// Don't remove entries from addedItems and replacementItems
// here...we want to be able to know the semantics of the
// added/replaced items after the fact.
var mod:CollectionModification = addedItems[itemWrapper] as CollectionModification;
// if this is not an added item, it might be a replacement item
if (!mod)
{
mod = replacementItems[itemWrapper] as CollectionModification;
if (mod)
{
// need more error checking here
mod.startShowingReplacementValue();
deltaLength++;
if (mod.modCount == 0)
removeModification(mod);
}
}
else if (removeModification(mod))
deltaLength++;
}
//--------------------------------------------------------------------------
//
// Private methods
//
//--------------------------------------------------------------------------
/**
* @private
* Determines if a change to a collection should be considered
* active or suppressed.
*
* Currently, this is just based on the <code>showPreserved</code>
* property.
*/
private function isActive(mod:CollectionModification):Boolean
{
// might eventually have more individual item processing here
// if we're showing removed, we have to include this modificatoin
// if we're showing added, we have to *exclude* it
return _showPreserved;
}
/**
* @private
*
* Removes a particular CollectionModification from the
* deltas array.
*/
private function removeModification(mod:CollectionModification):Boolean
{
for (var i:int = 0; i < deltas.length; i++)
{
if (deltas[i] == mod)
{
deltas.splice(i,1);
return true;
}
}
return false;
}
/**
* @private
*
* Does the work of modifying the object to handle a collectionEvent of
* collectionEventKind.REMOVE so that the removal can be ignored
*/
private function integrateRemovedElements(event:CollectionEvent, startItemIndex:int, endItemIndex:int):void
{
var i:int = 0;
var j:int = 0;
var ignoredElementCount:int = 0;
// var inserted:Boolean = false;
var insertCount:int = event.items.length;
// offset must be used when looking at mod indexes
var offset:int = 0;
while (i < deltas.length && j < insertCount)
{
var mod:CollectionModification = CollectionModification(deltas[i]);
var newMod:CollectionModification = new CollectionModification(event.location, event.items[j],CollectionModification.REMOVE);
removedItems[getUniqueItemWrapper(event.items[j],newMod,0)] = newMod;
if (offset != 0)
mod.index += offset;
// we want to insert after all deletes at this location but
// before all adds
// Adds coinciding with the deleted elements just become deletes
// (actually, want this behavior to depend on removeItemAction)
if ((mod.isRemove && mod.index <= newMod.index) || (!mod.isRemove && mod.index < newMod.index))
{
i++;
continue;
}
// we are deleting a previously added element
// we want to mark it as deleted and remove
else if ((!mod.isRemove) && (mod.index == newMod.index))
{
deltas.splice(i+j,1);
}
else
{
// this is a deletion or marked added element at a point
// after where we are adding elements,
// so we'll just splice in our deleted item.
deltas.splice(i+j,0,newMod);
i++;
}
offset--;
j++;
}
// when we get to this point, either we've inserted all the mods
// OR we're at the end of the list. So only one of the following
// two loops will be executed
while (i < deltas.length)
{
mod = CollectionModification(deltas[i++]);
mod.index += offset;
}
while (j < insertCount)
{
deltas.push(newMod = new CollectionModification(event.location, event.items[j],CollectionModification.REMOVE));
removedItems[getUniqueItemWrapper(event.items[j],newMod,0)] = newMod;
j++;
}
deltaLength += event.items.length - ignoredElementCount;
}
/**
* @private
*
* Does the work of modifying the object to handle a collectionEvent of
* collectionEventKind.ADD so that the addition can be ignored
*/
private function integrateAddedElements(event:CollectionEvent, startItemIndex:int, endItemIndex:int):void
{
var i:int = 0;
var j:int = 0;
var inserted:Boolean = false;
var insertCount:int = event.items.length;
// offset must be used when looking at mod indexes
var offset:int = 0;
// adding is easier than deleting...we just find the
// right place in our delta array, splice all the modifications,
// and update the indices of subsequent modifications
while (i < deltas.length && j < insertCount)
{
var mod:CollectionModification = CollectionModification(deltas[i]);
var newMod:CollectionModification = new CollectionModification(event.location + j, null,CollectionModification.ADD);
addedItems[getUniqueItemWrapper(event.items[j],newMod,0)] = newMod;
// we want to insert after all deletes at this location but
// before all adds
// Adds coinciding with the deleted elements just become deletes
// (actually, want this behavior to depend on removeItemAction)
if ((mod.isRemove && mod.index <= newMod.index) || (!mod.isRemove && mod.index < newMod.index))
{
i++;
continue;
}
// this is a deletion or marked added element at a point
// after where we are adding elements,
// so we'll just splice in our deleted item.
deltas.splice(i+j,0,newMod);
offset++;
j++;
i++;
}
// when we get to this point, either we've inserted all the mods
// OR we're at the end of the list. So only one of the following
// two loops will be executed
while (i < deltas.length)
{
mod = CollectionModification(deltas[i++]);
mod.index += offset;
}
while (j < insertCount)
{
deltas.push(newMod = new CollectionModification(event.location + j, null,CollectionModification.ADD));
addedItems[getUniqueItemWrapper(event.items[j],newMod,0)] = newMod;
j++;
}
deltaLength -= event.items.length;
}
/**
* @private
*
* Does the work of modifying the object to handle a collectionEvent of
* collectionEventKind.REPLACE so that the replacement can be ignored
*/
private function integrateReplacedElements(event:CollectionEvent, startItemIndex:int, endItemIndex:int):void
{
var i:int = 0;
var j:int = 0;
var inserted:Boolean = false;
var insertCount:int = event.items.length;
// offset must be used when looking at mod indexes
var offset:int = 0;
// adding is easier than deleting...we just find the
// right place in our delta array, splice all the modifications,
// and update the indices of subsequent modifications
while (i < deltas.length && j < insertCount)
{
var oldItem:Object = PropertyChangeEvent(event.items[j]).oldValue;
var newItem:Object = PropertyChangeEvent(event.items[j]).newValue;
var mod:CollectionModification = CollectionModification(deltas[i]);
var newMod:CollectionModification = new CollectionModification(event.location + j, oldItem,CollectionModification.REPLACE);
// we want to insert after all deletes at this location but
// before all adds
// Adds coinciding with the deleted elements just become deletes
// (actually, want this behavior to depend on removeItemAction)
if ((mod.isRemove && mod.index <= newMod.index) || (!mod.isRemove && mod.index < newMod.index))
{
i++;
continue;
}
if (((mod.modificationType == CollectionModification.ADD) || (mod.modificationType == CollectionModification.REPLACE))
&& (mod.index == newMod.index))
{
// we've founded an added element that is being replaced, or a replaced element that
// is being replaced again. We're just going to ignore this modification, so the existing effect
// if any should show the replacement element being added/replaced, rather than playing a new
// effect. (Not positive this will work.)
i++;
j++;
// might also need to do some cleanup here, if we're indexing our modifications
continue;
}
// this is a deletion or marked added element at a point
// after where we are adding elements,
// so we'll just splice in our replacement item.
deltas.splice(i+j,0,newMod);
replacedItems[getUniqueItemWrapper(oldItem,newMod,event.location + j)] = newMod;
replacementItems[getUniqueItemWrapper(newItem,newMod,event.location + j,true)] = newMod;
j++;
i++;
}
// when we get to this point, either we've inserted all the mods
// OR we're at the end of the list. So only one of the following
// two loops will be executed
while (j < insertCount)
{
oldItem = PropertyChangeEvent(event.items[j]).oldValue;
newItem = PropertyChangeEvent(event.items[j]).newValue;
deltas.push(newMod = new CollectionModification(event.location + j, oldItem,CollectionModification.REPLACE));
replacedItems[getUniqueItemWrapper(oldItem,newMod,event.location + j)] = newMod;
replacementItems[getUniqueItemWrapper(newItem,newMod,event.location + j,true)] = newMod;
j++;
}
}
private function getUniqueItemWrapper(item:Object,mod:CollectionModification, index:int, isReplacement:Boolean = false):Object
{
if (mod && (mod.isRemove || (mod.modificationType == CollectionModification.REPLACE && !isReplacement)))
{
if (!itemWrappersByCollectionMod[mod])
itemWrappersByCollectionMod[mod] = new ItemWrapper(item);
return itemWrappersByCollectionMod[mod];
}
// TODO (aharui): This is kind of a hack...clean up the code to simplify
if (mod && (mod.modificationType == CollectionModification.ADD))
index = mod.index;
if (!itemWrappersByIndex[index])
itemWrappersByIndex[index] = new ItemWrapper(item);
return itemWrappersByIndex[index];
}
}
}
import mx.collections.ModifiedCollectionView;
import mx.collections.CursorBookmark;
import flash.events.EventDispatcher;
import mx.collections.IViewCursor;
import mx.events.CollectionEvent;
import mx.collections.ICollectionView;
import mx.core.mx_internal;
import mx.collections.errors.CursorError;
import mx.collections.errors.CollectionViewError;
import mx.collections.errors.ItemPendingError;
import mx.resources.IResourceManager;
import mx.resources.ResourceManager;
import mx.events.CollectionEventKind;
import mx.events.FlexEvent;
import mx.collections.errors.CursorError;
import flash.display.InteractiveObject;
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 ModifiedCollectionView.
* This cursor wraps a cursor to the underlying collection, and maintains
* additional state.
*/
class ModifiedCollectionViewCursor 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.
*
* Creates the cursor for the view.
*
* @param view The ModifiedCollectionView for which this is a cursor.
*
* @param cursor A cursor into the underlying collection wrapped by the
* ModifiedCollectionView.
*
* @param current The item this cursor is currently pointing at.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function ModifiedCollectionViewCursor(view:ModifiedCollectionView, cursor:IViewCursor, current:Object)
{
super();
_view = view;
internalCursor = cursor;
if (cursor.beforeFirst && !current)
internalIndex = BEFORE_FIRST_INDEX;
else if (cursor.afterLast && !current)
internalIndex = AFTER_LAST_INDEX;
else
internalIndex = 0;
// This probably makes sense...
// _view.addEventListener(CollectionEvent.COLLECTION_CHANGE, collectionEventHandler, false, 0, true);
currentIndex = view.length > 0 ? 0 : AFTER_LAST_INDEX;
if (currentIndex == 0)
{
try
{
setCurrent(current,false);
}
catch(e:ItemPendingError)
{
currentIndex = BEFORE_FIRST_INDEX;
setCurrent(null, false);
}
}
}
//--------------------------------------------------------------------------
//
// Variables
//
//--------------------------------------------------------------------------
/**
* @private
*/
private var _view:ModifiedCollectionView;
/**
* @private
* An cursor into the underlying collection wrapped by the Modified
* collection view.
*/
public var internalCursor:IViewCursor;
/**
* @private
* The current overall index into the ModifiedCollectionView.
*/
mx_internal var currentIndex:int;
/**
* @private
* The position of the internalCursor in its ICollectionView.
* This is not part of the IViewCursor interface, so we
* maintain it independently.
*/
public var internalIndex: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;
return ModifiedCollectionView(view).getBookmark(this);
}
[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 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
{
// not implemented
return false;
}
/**
* 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 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
{
// not implemented
return false;
}
/**
* 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 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
{
// not implemented
return false;
}
/**
* 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
{
// not implemented
}
/**
* 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
* @see mx.collections.events.ItemAvailableEvent
* @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(ModifiedCollectionView(view).getItemAt(tempIndex));
setCurrent(ModifiedCollectionView(view).getWrappedItemUsingCursor(this,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
* @see mx.collections.events.ItemAvailableEvent
* @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(ModifiedCollectionView(view).getItemAt(tempIndex));
setCurrent(ModifiedCollectionView(view).getWrappedItemUsingCursor(this,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
{
// not implemented
return null;
}
/**
* 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
{
// trace("MCVC seek, bookmark = " + bookmark.value + ", offset = " + offset);
checkValid();
var message:String;
if (view.length == 0)
{
currentIndex = AFTER_LAST_INDEX;
setCurrent(null, false);
return;
}
var newIndex:int = currentIndex;
if (bookmark == CursorBookmark.FIRST)
{
newIndex = 0;
internalIndex = 0;
internalCursor.seek(CursorBookmark.FIRST);
}
else if (bookmark == CursorBookmark.LAST)
{
newIndex = view.length - 1;
internalCursor.seek(CursorBookmark.LAST);
}
else if (bookmark != CursorBookmark.CURRENT)
{
try
{
var mcvBookmark:ModifiedCollectionViewBookmark = bookmark as ModifiedCollectionViewBookmark;
newIndex = ModifiedCollectionView(view).getBookmarkIndex(bookmark);
if (!mcvBookmark || (newIndex < 0))
{
setCurrent(null);
message = resourceManager.getString(
"collections", "bookmarkInvalid");
throw new CursorError(message);
}
internalIndex = mcvBookmark.internalIndex;
internalCursor.seek(mcvBookmark.internalBookmark);
}
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 = ModifiedCollectionView(view).getWrappedItemUsingCursor(this,newIndex);
//newCurrent = ModifiedCollectionView(view).getItemAt(newIndex);
currentIndex = newIndex;
}
setCurrent(newCurrent);
}
//--------------------------------------------------------------------------
//
// Internal methods
//
//--------------------------------------------------------------------------
private function checkValid():void
{
if (invalid)
{
var message:String = resourceManager.getString(
"collections", "invalidCursor");
throw new CursorError(message);
}
}
/**
* @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 ModifiedCollectionView.
* Only the ModifiedCollectionView should construct this.
*/
class ModifiedCollectionViewBookmark extends CursorBookmark
{
mx_internal var index:int;
mx_internal var view:ModifiedCollectionView;
mx_internal var viewRevision:int;
// just as MCVCursor wraps a cursor into the underlying collection,
// this class wraps a bookmark for the wrapped cursor
mx_internal var internalBookmark:CursorBookmark;
mx_internal var internalIndex:int;
/**
* @private
*/
public function ModifiedCollectionViewBookmark(value:Object,
view:ModifiedCollectionView,
viewRevision:int,
index:int,
internalBookmark:CursorBookmark,
internalIndex:int)
{
super(value);
this.view = view;
this.viewRevision = viewRevision;
this.index = index;
this.internalBookmark = internalBookmark;
this.internalIndex = internalIndex;
}
/**
* Gets the approximate index of the item represented by this bookmark
* in its view. If the item has been paged out, this method could throw an
* ItemPendingError.
*
* @return The index of the item. If the item is not in the current view,
* this method returns -1. This method also returns -1 if index-based location
* retrieval 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);
}
}
// for now, no delta encoding, and no coalescing of blocks.
// not clear if we'd ever have to split a block -- probably
// not, since after deleting elements you can't insert one
// into the middle of that block...we'll just define whether
// an added element shows up after or before the deleted elements
// (i.e., if we delete the item at position 3, then insert at position
// 3, we have to decide whether the order is deleted-added or added-deleted)
/**
* @private
* Represents a single modification to a collection that a
* ModifiedCollectionView can either use or ignore in order to
* present "before" and "after" views of the change.
*
* A CollectionModification represents a single element only
* (add/remove/replace)
*/
class CollectionModification
{
public static const REMOVE:String = "remove";
public static const ADD:String = "add";
public static const REPLACE:String = "replace";
public function CollectionModification(index:int, item:Object, modificationType:String)
{
super();
this.index = index;
this.modificationType = modificationType;
if (modificationType != CollectionModification.ADD)
this.item = item;
if (modificationType == CollectionModification.REMOVE)
_modCount = 1;
else if (modificationType == CollectionModification.ADD)
_modCount = -1;
// replaces don't modify the count until we stop showing
// the old element or start showing the new element
// (if we do both, we're back to zero, and the CM can be
// discarded)
}
/**
* The point at which elements in the collection were removed or added
* (More precisely, the index of the a current element in the collection
* to which this modification is attached.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public var index:int;
/**
* Removed element, if applicable
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public var item:Object = null;
public var modificationType:String = null;
private var _modCount:int = 0;
// shouldn't be public
public var showOldReplace:Boolean = true;
public var showNewReplace:Boolean = false;
public function get isRemove():Boolean
{
return (modificationType == CollectionModification.REMOVE);
}
/**
* For CollectionModifications representing replaced elements
* in a collection, starts showing the replaced value.
*
* For replaces, the original and replacement values may
* be shown independently.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function startShowingReplacementValue():void
{
showNewReplace = true;
// should do some error checking here, in case this function is called twice
_modCount++;
}
/**
* For CollectionModifications representing replaced elements
* in a collection, stops showing the replaced value.
*
* For replaces, the original and replacement values may
* be shown independently.
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function stopShowingReplacedValue():void
{
showOldReplace = false;
// should do some error checking here, in case this function is called twice
_modCount--;
}
/**
* The number of removed elements being preserved in the modified collection,
* minus the number of added elements not in the original collection
*
* @langversion 3.0
* @playerversion Flash 9
* @playerversion AIR 1.1
* @productversion Flex 3
*/
public function get modCount():int
{
return _modCount;
}
}