| // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file |
| // for details. All rights reserved. Use of this source code is governed by a |
| // BSD-style license that can be found in the LICENSE file. |
| |
| part of observe; |
| |
| /** |
| * Represents an observable list of model values. If any items are added, |
| * removed, or replaced, then observers that are listening to [changes] |
| * will be notified. |
| */ |
| class ObservableList<E> extends ListBase<E> with ChangeNotifierMixin { |
| List<ListChangeRecord> _listRecords; |
| |
| /** The inner [List<E>] with the actual storage. */ |
| final List<E> _list; |
| |
| /** |
| * Creates an observable list of the given [length]. |
| * |
| * If no [length] argument is supplied an extendable list of |
| * length 0 is created. |
| * |
| * If a [length] argument is supplied, a fixed size list of that |
| * length is created. |
| */ |
| ObservableList([int length]) |
| : _list = length != null ? new List<E>(length) : <E>[]; |
| |
| /** |
| * Creates an observable list with the elements of [other]. The order in |
| * the list will be the order provided by the iterator of [other]. |
| */ |
| factory ObservableList.from(Iterable<E> other) => |
| new ObservableList<E>()..addAll(other); |
| |
| int get length => _list.length; |
| |
| set length(int value) { |
| int len = _list.length; |
| if (len == value) return; |
| |
| // Produce notifications if needed |
| if (hasObservers) { |
| if (value < len) { |
| // Remove items, then adjust length. Note the reverse order. |
| _recordChange(new ListChangeRecord(value, removedCount: len - value)); |
| } else { |
| // Adjust length then add items |
| _recordChange(new ListChangeRecord(len, addedCount: value - len)); |
| } |
| } |
| |
| _list.length = value; |
| } |
| |
| E operator [](int index) => _list[index]; |
| |
| void operator []=(int index, E value) { |
| var oldValue = _list[index]; |
| if (hasObservers) { |
| _recordChange(new ListChangeRecord(index, addedCount: 1, |
| removedCount: 1)); |
| } |
| _list[index] = value; |
| } |
| |
| // The following methods are here so that we can provide nice change events. |
| |
| void setAll(int index, Iterable<E> iterable) { |
| if (iterable is! List && iterable is! Set) { |
| iterable = iterable.toList(); |
| } |
| var len = iterable.length; |
| _list.setAll(index, iterable); |
| if (hasObservers && len > 0) { |
| _recordChange( |
| new ListChangeRecord(index, addedCount: len, removedCount: len)); |
| } |
| } |
| |
| void add(E value) { |
| int len = _list.length; |
| if (hasObservers) { |
| _recordChange(new ListChangeRecord(len, addedCount: 1)); |
| } |
| |
| _list.add(value); |
| } |
| |
| void addAll(Iterable<E> iterable) { |
| int len = _list.length; |
| _list.addAll(iterable); |
| int added = _list.length - len; |
| if (hasObservers && added > 0) { |
| _recordChange(new ListChangeRecord(len, addedCount: added)); |
| } |
| } |
| |
| bool remove(Object element) { |
| for (int i = 0; i < this.length; i++) { |
| if (this[i] == element) { |
| removeRange(i, i + 1); |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| void removeRange(int start, int end) { |
| _rangeCheck(start, end); |
| int length = end - start; |
| _list.setRange(start, this.length - length, this, end); |
| |
| int len = _list.length; |
| _list.length -= length; |
| if (hasObservers && length > 0) { |
| _recordChange(new ListChangeRecord(start, removedCount: length)); |
| } |
| } |
| |
| void insertAll(int index, Iterable<E> iterable) { |
| if (index < 0 || index > length) { |
| throw new RangeError.range(index, 0, length); |
| } |
| // TODO(floitsch): we can probably detect more cases. |
| if (iterable is! List && iterable is! Set) { |
| iterable = iterable.toList(); |
| } |
| int insertionLength = iterable.length; |
| // There might be errors after the length change, in which case the list |
| // will end up being modified but the operation not complete. Unless we |
| // always go through a "toList" we can't really avoid that. |
| int len = _list.length; |
| _list.length += insertionLength; |
| |
| _list.setRange(index + insertionLength, this.length, this, index); |
| _list.setAll(index, iterable); |
| |
| if (hasObservers && insertionLength > 0) { |
| _recordChange(new ListChangeRecord(index, addedCount: insertionLength)); |
| } |
| } |
| |
| void insert(int index, E element) { |
| if (index < 0 || index > length) { |
| throw new RangeError.range(index, 0, length); |
| } |
| if (index == length) { |
| add(element); |
| return; |
| } |
| // We are modifying the length just below the is-check. Without the check |
| // Array.copy could throw an exception, leaving the list in a bad state |
| // (with a length that has been increased, but without a new element). |
| if (index is! int) throw new ArgumentError(index); |
| _list.length++; |
| _list.setRange(index + 1, length, this, index); |
| if (hasObservers) { |
| _recordChange(new ListChangeRecord(index, addedCount: 1)); |
| } |
| _list[index] = element; |
| } |
| |
| |
| E removeAt(int index) { |
| E result = this[index]; |
| removeRange(index, index + 1); |
| return result; |
| } |
| |
| void _rangeCheck(int start, int end) { |
| if (start < 0 || start > this.length) { |
| throw new RangeError.range(start, 0, this.length); |
| } |
| if (end < start || end > this.length) { |
| throw new RangeError.range(end, start, this.length); |
| } |
| } |
| |
| void _recordChange(ListChangeRecord record) { |
| if (_listRecords == null) { |
| _listRecords = []; |
| runAsync(deliverChanges); |
| } |
| _listRecords.add(record); |
| } |
| |
| bool deliverChanges() { |
| if (_listRecords == null) return false; |
| _summarizeRecords(); |
| return super.deliverChanges(); |
| } |
| |
| /** |
| * We need to summarize change records. Consumers of these records want to |
| * apply the batch sequentially, and ensure that they can find inserted |
| * items by looking at that position in the list. This property does not |
| * hold in our record-as-you-go records. Consider: |
| * |
| * var model = toObservable(['a', 'b']); |
| * model.removeAt(1); |
| * model.insertAll(0, ['c', 'd', 'e']); |
| * model.removeRange(1, 3); |
| * model.insert(1, 'f'); |
| * |
| * Here, we inserted some records and then removed some of them. |
| * If someone processed these records naively, they would "play back" the |
| * insert incorrectly, because those items will be shifted. |
| * |
| * We summarize changes using a straightforward technique: |
| * Simulate the moves and use the final item positions to synthesize a |
| * new list of changes records. This has the advantage of not depending |
| * on the actual *values*, so we don't need to perform N^2 edit |
| */ |
| // TODO(jmesserly): there's probably something smarter here, but this |
| // algorithm is pretty simple. It has complexity equivalent to the original |
| // list modifications. |
| // One simple idea: we can simply update the index map as we do the operations |
| // to the list, then produce the records at the end. |
| void _summarizeRecords() { |
| int oldLength = length; |
| for (var r in _listRecords) { |
| oldLength += r.removedCount - r.addedCount; |
| } |
| |
| if (length != oldLength) { |
| notifyPropertyChange(const Symbol('length'), oldLength, length); |
| } |
| |
| if (_listRecords.length == 1) { |
| notifyChange(_listRecords[0]); |
| _listRecords = null; |
| return; |
| } |
| |
| var items = []; |
| for (int i = 0; i < oldLength; i++) items.add(i); |
| for (var r in _listRecords) { |
| items.removeRange(r.index, r.index + r.removedCount); |
| |
| // Represent inserts with -1. |
| items.insertAll(r.index, new List.filled(r.addedCount, -1)); |
| } |
| assert(items.length == length); |
| |
| _listRecords = null; |
| |
| int index = 0; |
| int offset = 0; |
| while (index < items.length) { |
| // Skip unchanged items. |
| while (index < items.length && items[index] == index + offset) { |
| index++; |
| } |
| |
| // Find inserts |
| int startIndex = index; |
| while (index < items.length && items[index] == -1) { |
| index++; |
| } |
| |
| int added = index - startIndex; |
| |
| // Use the delta between our actual and expected position to determine |
| // how much was removed. |
| int actualItem = index < items.length ? items[index] : oldLength; |
| int expectedItem = startIndex + offset; |
| |
| int removed = actualItem - expectedItem; |
| |
| if (added > 0 || removed > 0) { |
| notifyChange(new ListChangeRecord(startIndex, addedCount: added, |
| removedCount: removed)); |
| } |
| |
| offset += removed - added; |
| } |
| } |
| } |