blob: 76cf06bf0ebe8fc4a3122f9848d4f9d5305022de [file] [log] [blame]
// 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.
library observe.src.observable_list;
import 'dart:async';
import 'dart:collection' show ListBase, UnmodifiableListView;
import 'package:observe/observe.dart';
import 'list_diff.dart' show projectListSplices, calcSplices;
/**
* 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 ChangeNotifier {
List<ListChangeRecord> _listRecords;
StreamController _listChanges;
/** 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);
/**
* The stream of summarized list changes, delivered asynchronously.
*
* Each list change record contains information about an individual mutation.
* The records are projected so they can be applied sequentially. For example,
* this set of mutations:
*
* var model = new ObservableList.from(['a', 'b']);
* model.listChanges.listen((records) => records.forEach(print));
* model.removeAt(1);
* model.insertAll(0, ['c', 'd', 'e']);
* model.removeRange(1, 3);
* model.insert(1, 'f');
*
* The change records will be summarized so they can be "played back", using
* the final list positions to figure out which item was added:
*
* #<ListChangeRecord index: 0, removed: [], addedCount: 2>
* #<ListChangeRecord index: 3, removed: [b], addedCount: 0>
*
* [deliverChanges] can be called to force synchronous delivery.
*/
Stream<List<ListChangeRecord>> get listChanges {
if (_listChanges == null) {
// TODO(jmesserly): split observed/unobserved notions?
_listChanges = new StreamController.broadcast(sync: true,
onCancel: () { _listChanges = null; });
}
return _listChanges.stream;
}
bool get _hasListObservers =>
_listChanges != null && _listChanges.hasListener;
@reflectable int get length => _list.length;
@reflectable set length(int value) {
int len = _list.length;
if (len == value) return;
// Produce notifications if needed
notifyPropertyChange(#length, len, value);
if (_hasListObservers) {
if (value < len) {
_recordChange(new ListChangeRecord(this, value,
removed: _list.getRange(value, len).toList()));
} else {
_recordChange(new ListChangeRecord(this, len, addedCount: value - len));
}
}
_list.length = value;
}
@reflectable E operator [](int index) => _list[index];
@reflectable void operator []=(int index, E value) {
var oldValue = _list[index];
if (_hasListObservers) {
_recordChange(new ListChangeRecord(this, index, addedCount: 1,
removed: [oldValue]));
}
_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;
if (_hasListObservers && len > 0) {
_recordChange(new ListChangeRecord(this, index, addedCount: len,
removed: _list.getRange(index, len).toList()));
}
_list.setAll(index, iterable);
}
void add(E value) {
int len = _list.length;
notifyPropertyChange(#length, len, len + 1);
if (_hasListObservers) {
_recordChange(new ListChangeRecord(this, len, addedCount: 1));
}
_list.add(value);
}
void addAll(Iterable<E> iterable) {
int len = _list.length;
_list.addAll(iterable);
notifyPropertyChange(#length, len, _list.length);
int added = _list.length - len;
if (_hasListObservers && added > 0) {
_recordChange(new ListChangeRecord(this, 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 rangeLength = end - start;
int len = _list.length;
notifyPropertyChange(#length, len, len - rangeLength);
if (_hasListObservers && rangeLength > 0) {
_recordChange(new ListChangeRecord(this, start,
removed: _list.getRange(start, end).toList()));
}
_list.removeRange(start, end);
}
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);
notifyPropertyChange(#length, len, _list.length);
if (_hasListObservers && insertionLength > 0) {
_recordChange(new ListChangeRecord(this, 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);
notifyPropertyChange(#length, _list.length - 1, _list.length);
if (_hasListObservers) {
_recordChange(new ListChangeRecord(this, 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 (!_hasListObservers) return;
if (_listRecords == null) {
_listRecords = [];
scheduleMicrotask(deliverListChanges);
}
_listRecords.add(record);
}
bool deliverListChanges() {
if (_listRecords == null) return false;
var records = projectListSplices(this, _listRecords);
_listRecords = null;
if (_hasListObservers) {
_listChanges.add(new UnmodifiableListView<ListChangeRecord>(records));
return true;
}
return false;
}
/**
* Calculates the changes to the list, if lacking individual splice mutation
* information.
*
* This is not needed for change records produced by [ObservableList] itself,
* but it can be used if the list instance was replaced by another list.
*
* The minimal set of splices can be synthesized given the previous state and
* final state of a list. The basic approach is to calculate the edit distance
* matrix and choose the shortest path through it.
*
* Complexity is `O(l * p)` where `l` is the length of the current list and
* `p` is the length of the old list.
*/
static List<ListChangeRecord> calculateChangeRecords(
List<Object> oldValue, List<Object> newValue) =>
calcSplices(newValue, 0, newValue.length, oldValue, 0, oldValue.length);
}