| Index: observatory_pub_packages/observe/src/observable_list.dart
|
| ===================================================================
|
| --- observatory_pub_packages/observe/src/observable_list.dart (revision 0)
|
| +++ observatory_pub_packages/observe/src/observable_list.dart (working copy)
|
| @@ -0,0 +1,304 @@
|
| +// 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
|
| + _notifyChangeLength(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;
|
| + }
|
| +
|
| + // Forwarders so we can reflect on the properties.
|
| + @reflectable bool get isEmpty => super.isEmpty;
|
| + @reflectable bool get isNotEmpty => super.isNotEmpty;
|
| +
|
| + // TODO(jmesserly): should we support first/last/single? They're kind of
|
| + // dangerous to use in a path because they throw exceptions. Also we'd need
|
| + // to produce property change notifications which seems to conflict with our
|
| + // existing list notifications.
|
| +
|
| + // 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;
|
| + _notifyChangeLength(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);
|
| +
|
| + _notifyChangeLength(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;
|
| +
|
| + _notifyChangeLength(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);
|
| +
|
| + _notifyChangeLength(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);
|
| +
|
| + _notifyChangeLength(_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);
|
| + }
|
| +
|
| + void _notifyChangeLength(int oldValue, int newValue) {
|
| + notifyPropertyChange(#length, oldValue, newValue);
|
| + notifyPropertyChange(#isEmpty, oldValue == 0, newValue == 0);
|
| + notifyPropertyChange(#isNotEmpty, oldValue != 0, newValue != 0);
|
| + }
|
| +
|
| + /// Deprecated. Name had a typo, use [discardListChanges] instead.
|
| + @deprecated
|
| + void discardListChages() => discardListChanges();
|
| +
|
| + void discardListChanges() {
|
| + // Leave _listRecords set so we don't schedule another delivery.
|
| + if (_listRecords != null) _listRecords = [];
|
| + }
|
| +
|
| + bool deliverListChanges() {
|
| + if (_listRecords == null) return false;
|
| + var records = projectListSplices(this, _listRecords);
|
| + _listRecords = null;
|
| +
|
| + if (_hasListObservers && !records.isEmpty) {
|
| + _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);
|
| +
|
| + /// Updates the [previous] list using the change [records]. For added items,
|
| + /// the [current] list is used to find the current value.
|
| + static void applyChangeRecords(List<Object> previous, List<Object> current,
|
| + List<ListChangeRecord> changeRecords) {
|
| +
|
| + if (identical(previous, current)) {
|
| + throw new ArgumentError("can't use same list for previous and current");
|
| + }
|
| +
|
| + for (var change in changeRecords) {
|
| + int addEnd = change.index + change.addedCount;
|
| + int removeEnd = change.index + change.removed.length;
|
| +
|
| + var addedItems = current.getRange(change.index, addEnd);
|
| + previous.replaceRange(change.index, removeEnd, addedItems);
|
| + }
|
| + }
|
| +}
|
|
|