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); |
+ } |
+ } |
+} |