//////////////////////////////////////////////////////////////////////////////// // // 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 showPreserved * 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 ICollectionView * * @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 null. * * @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 * current property. * The bookmark can be used to move the cursor to a previously visited * item, or one relative to it (see the seek() 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. * findAny() can only be called on sorted views, if the view * isn't sorted a CursorError will be thrown. *

* 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 findFirst() or * findLast(). * 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 true if * the item can be found and false otherwise. * If the data is not local and an asynchronous operation must be performed, * an ItemPendingError 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. * findFirst() can only be called on sorted views, if the view * isn't sorted a CursorError will be thrown. *

* 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 findAny() 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 true if the item can be found and false otherwise. * If the data is not local and an asynchronous operation must be performed, * an ItemPendingError 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. * findLast() can only be called on sorted views, if the view * isn't sorted a CursorError will be thrown. *

* 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 findAny() 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 true if the item can be found and false otherwise. * If the data is not local and an asynchronous operation must be performed, * an ItemPendingError 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 afterLast the insertion * will happen at the end of the View. If the cursor is * beforeFirst 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 current 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 * ItemPendingError 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 *

     *    var myArrayCollection:ICollectionView = new ArrayCollection(["Bobby", "Mark", "Trevor", "Jacey", "Tyler"]);
     *    var cursor:IViewCursor = myArrayCollection.createCursor();
     *    while (!cursor.afterLast)
     *    {
     *       trace(cursor.current);
     *       cursor.moveNext();
     *     }
     *  
* * @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 current 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 * ItemPendingError 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 *
     *     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();
     *      }
     *  
* * @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 * beforeFirst or afterLast 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 * ItemPendingError will be thrown. See the ItemPendingError docs * as well as the collections documentation for more information on using the * ItemPendingError. * * * @param bookmark CursorBookmark reference to marker information that * allows repositioning to a specific location. * In addition to supplying a value returned from the bookmark * property, there are three constant bookmark values that can be * specified: * * @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; } /** * 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); } } // 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; } }