OLD | NEW |
| (Empty) |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | |
2 // for details. All rights reserved. Use of this source code is governed by a | |
3 // BSD-style license that can be found in the LICENSE file. | |
4 | |
5 library observe.src.observable_list; | |
6 | |
7 import 'dart:async'; | |
8 import 'dart:collection' show ListBase, UnmodifiableListView; | |
9 import 'package:observe/observe.dart'; | |
10 import 'list_diff.dart' show projectListSplices, calcSplices; | |
11 | |
12 /// Represents an observable list of model values. If any items are added, | |
13 /// removed, or replaced, then observers that are listening to [changes] | |
14 /// will be notified. | |
15 class ObservableList<E> extends ListBase<E> with ChangeNotifier { | |
16 List<ListChangeRecord> _listRecords; | |
17 | |
18 StreamController _listChanges; | |
19 | |
20 /// The inner [List<E>] with the actual storage. | |
21 final List<E> _list; | |
22 | |
23 /// Creates an observable list of the given [length]. | |
24 /// | |
25 /// If no [length] argument is supplied an extendable list of | |
26 /// length 0 is created. | |
27 /// | |
28 /// If a [length] argument is supplied, a fixed size list of that | |
29 /// length is created. | |
30 ObservableList([int length]) | |
31 : _list = length != null ? new List<E>(length) : <E>[]; | |
32 | |
33 /// Creates an observable list with the elements of [other]. The order in | |
34 /// the list will be the order provided by the iterator of [other]. | |
35 factory ObservableList.from(Iterable<E> other) => | |
36 new ObservableList<E>()..addAll(other); | |
37 | |
38 /// The stream of summarized list changes, delivered asynchronously. | |
39 /// | |
40 /// Each list change record contains information about an individual mutation. | |
41 /// The records are projected so they can be applied sequentially. For | |
42 /// example, this set of mutations: | |
43 /// | |
44 /// var model = new ObservableList.from(['a', 'b']); | |
45 /// model.listChanges.listen((records) => records.forEach(print)); | |
46 /// model.removeAt(1); | |
47 /// model.insertAll(0, ['c', 'd', 'e']); | |
48 /// model.removeRange(1, 3); | |
49 /// model.insert(1, 'f'); | |
50 /// | |
51 /// The change records will be summarized so they can be "played back", using | |
52 /// the final list positions to figure out which item was added: | |
53 /// | |
54 /// #<ListChangeRecord index: 0, removed: [], addedCount: 2> | |
55 /// #<ListChangeRecord index: 3, removed: [b], addedCount: 0> | |
56 /// | |
57 /// [deliverChanges] can be called to force synchronous delivery. | |
58 Stream<List<ListChangeRecord>> get listChanges { | |
59 if (_listChanges == null) { | |
60 // TODO(jmesserly): split observed/unobserved notions? | |
61 _listChanges = new StreamController.broadcast(sync: true, | |
62 onCancel: () { _listChanges = null; }); | |
63 } | |
64 return _listChanges.stream; | |
65 } | |
66 | |
67 bool get hasListObservers => | |
68 _listChanges != null && _listChanges.hasListener; | |
69 | |
70 @reflectable int get length => _list.length; | |
71 | |
72 @reflectable set length(int value) { | |
73 int len = _list.length; | |
74 if (len == value) return; | |
75 | |
76 // Produce notifications if needed | |
77 _notifyChangeLength(len, value); | |
78 if (hasListObservers) { | |
79 if (value < len) { | |
80 _recordChange(new ListChangeRecord(this, value, | |
81 removed: _list.getRange(value, len).toList())); | |
82 } else { | |
83 _recordChange(new ListChangeRecord(this, len, addedCount: value - len)); | |
84 } | |
85 } | |
86 | |
87 _list.length = value; | |
88 } | |
89 | |
90 @reflectable E operator [](int index) => _list[index]; | |
91 | |
92 @reflectable void operator []=(int index, E value) { | |
93 var oldValue = _list[index]; | |
94 if (hasListObservers && oldValue != value) { | |
95 _recordChange(new ListChangeRecord(this, index, addedCount: 1, | |
96 removed: [oldValue])); | |
97 } | |
98 _list[index] = value; | |
99 } | |
100 | |
101 // Forwarders so we can reflect on the properties. | |
102 @reflectable bool get isEmpty => super.isEmpty; | |
103 @reflectable bool get isNotEmpty => super.isNotEmpty; | |
104 | |
105 // TODO(jmesserly): should we support first/last/single? They're kind of | |
106 // dangerous to use in a path because they throw exceptions. Also we'd need | |
107 // to produce property change notifications which seems to conflict with our | |
108 // existing list notifications. | |
109 | |
110 // The following methods are here so that we can provide nice change events. | |
111 | |
112 void setAll(int index, Iterable<E> iterable) { | |
113 if (iterable is! List && iterable is! Set) { | |
114 iterable = iterable.toList(); | |
115 } | |
116 var len = iterable.length; | |
117 if (hasListObservers && len > 0) { | |
118 _recordChange(new ListChangeRecord(this, index, addedCount: len, | |
119 removed: _list.getRange(index, len).toList())); | |
120 } | |
121 _list.setAll(index, iterable); | |
122 } | |
123 | |
124 void add(E value) { | |
125 int len = _list.length; | |
126 _notifyChangeLength(len, len + 1); | |
127 if (hasListObservers) { | |
128 _recordChange(new ListChangeRecord(this, len, addedCount: 1)); | |
129 } | |
130 | |
131 _list.add(value); | |
132 } | |
133 | |
134 void addAll(Iterable<E> iterable) { | |
135 int len = _list.length; | |
136 _list.addAll(iterable); | |
137 | |
138 _notifyChangeLength(len, _list.length); | |
139 | |
140 int added = _list.length - len; | |
141 if (hasListObservers && added > 0) { | |
142 _recordChange(new ListChangeRecord(this, len, addedCount: added)); | |
143 } | |
144 } | |
145 | |
146 bool remove(Object element) { | |
147 for (int i = 0; i < this.length; i++) { | |
148 if (this[i] == element) { | |
149 removeRange(i, i + 1); | |
150 return true; | |
151 } | |
152 } | |
153 return false; | |
154 } | |
155 | |
156 void removeRange(int start, int end) { | |
157 _rangeCheck(start, end); | |
158 int rangeLength = end - start; | |
159 int len = _list.length; | |
160 | |
161 _notifyChangeLength(len, len - rangeLength); | |
162 if (hasListObservers && rangeLength > 0) { | |
163 _recordChange(new ListChangeRecord(this, start, | |
164 removed: _list.getRange(start, end).toList())); | |
165 } | |
166 | |
167 _list.removeRange(start, end); | |
168 } | |
169 | |
170 void insertAll(int index, Iterable<E> iterable) { | |
171 if (index < 0 || index > length) { | |
172 throw new RangeError.range(index, 0, length); | |
173 } | |
174 // TODO(floitsch): we can probably detect more cases. | |
175 if (iterable is! List && iterable is! Set) { | |
176 iterable = iterable.toList(); | |
177 } | |
178 int insertionLength = iterable.length; | |
179 // There might be errors after the length change, in which case the list | |
180 // will end up being modified but the operation not complete. Unless we | |
181 // always go through a "toList" we can't really avoid that. | |
182 int len = _list.length; | |
183 _list.length += insertionLength; | |
184 | |
185 _list.setRange(index + insertionLength, this.length, this, index); | |
186 _list.setAll(index, iterable); | |
187 | |
188 _notifyChangeLength(len, _list.length); | |
189 | |
190 if (hasListObservers && insertionLength > 0) { | |
191 _recordChange(new ListChangeRecord(this, index, | |
192 addedCount: insertionLength)); | |
193 } | |
194 } | |
195 | |
196 void insert(int index, E element) { | |
197 if (index < 0 || index > length) { | |
198 throw new RangeError.range(index, 0, length); | |
199 } | |
200 if (index == length) { | |
201 add(element); | |
202 return; | |
203 } | |
204 // We are modifying the length just below the is-check. Without the check | |
205 // Array.copy could throw an exception, leaving the list in a bad state | |
206 // (with a length that has been increased, but without a new element). | |
207 if (index is! int) throw new ArgumentError(index); | |
208 _list.length++; | |
209 _list.setRange(index + 1, length, this, index); | |
210 | |
211 _notifyChangeLength(_list.length - 1, _list.length); | |
212 if (hasListObservers) { | |
213 _recordChange(new ListChangeRecord(this, index, addedCount: 1)); | |
214 } | |
215 _list[index] = element; | |
216 } | |
217 | |
218 | |
219 E removeAt(int index) { | |
220 E result = this[index]; | |
221 removeRange(index, index + 1); | |
222 return result; | |
223 } | |
224 | |
225 void _rangeCheck(int start, int end) { | |
226 if (start < 0 || start > this.length) { | |
227 throw new RangeError.range(start, 0, this.length); | |
228 } | |
229 if (end < start || end > this.length) { | |
230 throw new RangeError.range(end, start, this.length); | |
231 } | |
232 } | |
233 | |
234 void _recordChange(ListChangeRecord record) { | |
235 if (!hasListObservers) return; | |
236 | |
237 if (_listRecords == null) { | |
238 _listRecords = []; | |
239 scheduleMicrotask(deliverListChanges); | |
240 } | |
241 _listRecords.add(record); | |
242 } | |
243 | |
244 void _notifyChangeLength(int oldValue, int newValue) { | |
245 notifyPropertyChange(#length, oldValue, newValue); | |
246 notifyPropertyChange(#isEmpty, oldValue == 0, newValue == 0); | |
247 notifyPropertyChange(#isNotEmpty, oldValue != 0, newValue != 0); | |
248 } | |
249 | |
250 /// Deprecated. Name had a typo, use [discardListChanges] instead. | |
251 @deprecated | |
252 void discardListChages() => discardListChanges(); | |
253 | |
254 void discardListChanges() { | |
255 // Leave _listRecords set so we don't schedule another delivery. | |
256 if (_listRecords != null) _listRecords = []; | |
257 } | |
258 | |
259 bool deliverListChanges() { | |
260 if (_listRecords == null) return false; | |
261 var records = projectListSplices(this, _listRecords); | |
262 _listRecords = null; | |
263 | |
264 if (hasListObservers && !records.isEmpty) { | |
265 _listChanges.add(new UnmodifiableListView<ListChangeRecord>(records)); | |
266 return true; | |
267 } | |
268 return false; | |
269 } | |
270 | |
271 /// Calculates the changes to the list, if lacking individual splice mutation | |
272 /// information. | |
273 /// | |
274 /// This is not needed for change records produced by [ObservableList] itself, | |
275 /// but it can be used if the list instance was replaced by another list. | |
276 /// | |
277 /// The minimal set of splices can be synthesized given the previous state and | |
278 /// final state of a list. The basic approach is to calculate the edit | |
279 /// distance matrix and choose the shortest path through it. | |
280 /// | |
281 /// Complexity is `O(l * p)` where `l` is the length of the current list and | |
282 /// `p` is the length of the old list. | |
283 static List<ListChangeRecord> calculateChangeRecords( | |
284 List<Object> oldValue, List<Object> newValue) => | |
285 calcSplices(newValue, 0, newValue.length, oldValue, 0, oldValue.length); | |
286 | |
287 /// Updates the [previous] list using the change [records]. For added items, | |
288 /// the [current] list is used to find the current value. | |
289 static void applyChangeRecords(List<Object> previous, List<Object> current, | |
290 List<ListChangeRecord> changeRecords) { | |
291 | |
292 if (identical(previous, current)) { | |
293 throw new ArgumentError("can't use same list for previous and current"); | |
294 } | |
295 | |
296 for (var change in changeRecords) { | |
297 int addEnd = change.index + change.addedCount; | |
298 int removeEnd = change.index + change.removed.length; | |
299 | |
300 var addedItems = current.getRange(change.index, addEnd); | |
301 previous.replaceRange(change.index, removeEnd, addedItems); | |
302 } | |
303 } | |
304 } | |
OLD | NEW |