| 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 part of mdv_observe; | |
| 6 | |
| 7 /** | |
| 8 * Represents an observable list of model values. If any items are added, | |
| 9 * removed, or replaced, then observers that are listening to [changes] | |
| 10 * will be notified. | |
| 11 */ | |
| 12 // TODO(jmesserly): remove implements List<E> once we can extend ListBase<E> | |
| 13 class ObservableList<E> extends _ListBaseWorkaround with ObservableMixin | |
| 14 implements List<E> { | |
| 15 List<ListChangeRecord> _records; | |
| 16 | |
| 17 static const _LENGTH = const Symbol('length'); | |
| 18 | |
| 19 /** The inner [List<E>] with the actual storage. */ | |
| 20 final List<E> _list; | |
| 21 | |
| 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 */ | |
| 31 ObservableList([int length]) | |
| 32 : _list = length != null ? new List<E>(length) : <E>[]; | |
| 33 | |
| 34 /** | |
| 35 * Creates an observable list with the elements of [other]. The order in | |
| 36 * the list will be the order provided by the iterator of [other]. | |
| 37 */ | |
| 38 factory ObservableList.from(Iterable<E> other) => | |
| 39 new ObservableList<E>()..addAll(other); | |
| 40 | |
| 41 // TODO(jmesserly): remove once we have mirrors | |
| 42 getValueWorkaround(key) => key == _LENGTH ? length : null; | |
| 43 | |
| 44 setValueWorkaround(key, value) { | |
| 45 if (key == _LENGTH) length = value; | |
| 46 } | |
| 47 | |
| 48 int get length => _list.length; | |
| 49 | |
| 50 set length(int value) { | |
| 51 int len = _list.length; | |
| 52 if (len == value) return; | |
| 53 | |
| 54 // Produce notifications if needed | |
| 55 if (hasObservers) { | |
| 56 if (value < len) { | |
| 57 // Remove items, then adjust length. Note the reverse order. | |
| 58 _recordChange(new ListChangeRecord(value, removedCount: len - value)); | |
| 59 } else { | |
| 60 // Adjust length then add items | |
| 61 _recordChange(new ListChangeRecord(len, addedCount: value - len)); | |
| 62 } | |
| 63 } | |
| 64 | |
| 65 _list.length = value; | |
| 66 } | |
| 67 | |
| 68 E operator [](int index) => _list[index]; | |
| 69 | |
| 70 void operator []=(int index, E value) { | |
| 71 var oldValue = _list[index]; | |
| 72 if (hasObservers) { | |
| 73 _recordChange(new ListChangeRecord(index, addedCount: 1, removedCount: 1))
; | |
| 74 } | |
| 75 _list[index] = value; | |
| 76 } | |
| 77 | |
| 78 // The following methods are here so that we can provide nice change events. | |
| 79 | |
| 80 void setAll(int index, Iterable<E> iterable) { | |
| 81 if (iterable is! List && iterable is! Set) { | |
| 82 iterable = iterable.toList(); | |
| 83 } | |
| 84 var len = iterable.length; | |
| 85 _list.setAll(index, iterable); | |
| 86 if (hasObservers && len > 0) { | |
| 87 _recordChange( | |
| 88 new ListChangeRecord(index, addedCount: len, removedCount: len)); | |
| 89 } | |
| 90 } | |
| 91 | |
| 92 void add(E value) { | |
| 93 int len = _list.length; | |
| 94 if (hasObservers) { | |
| 95 _recordChange(new ListChangeRecord(len, addedCount: 1)); | |
| 96 } | |
| 97 | |
| 98 _list.add(value); | |
| 99 } | |
| 100 | |
| 101 void addAll(Iterable<E> iterable) { | |
| 102 int len = _list.length; | |
| 103 _list.addAll(iterable); | |
| 104 int added = _list.length - len; | |
| 105 if (hasObservers && added > 0) { | |
| 106 _recordChange(new ListChangeRecord(len, addedCount: added)); | |
| 107 } | |
| 108 } | |
| 109 | |
| 110 bool remove(Object element) { | |
| 111 for (int i = 0; i < this.length; i++) { | |
| 112 if (this[i] == element) { | |
| 113 removeRange(i, 1); | |
| 114 return true; | |
| 115 } | |
| 116 } | |
| 117 return false; | |
| 118 } | |
| 119 | |
| 120 void removeRange(int start, int end) { | |
| 121 _rangeCheck(start, end); | |
| 122 int length = end - start; | |
| 123 _list.setRange(start, this.length - length, this, end); | |
| 124 | |
| 125 int len = _list.length; | |
| 126 _list.length -= length; | |
| 127 if (hasObservers && length > 0) { | |
| 128 _recordChange(new ListChangeRecord(start, removedCount: length)); | |
| 129 } | |
| 130 } | |
| 131 | |
| 132 void insertAll(int index, Iterable<E> iterable) { | |
| 133 if (index < 0 || index > length) { | |
| 134 throw new RangeError.range(index, 0, length); | |
| 135 } | |
| 136 // TODO(floitsch): we can probably detect more cases. | |
| 137 if (iterable is! List && iterable is! Set) { | |
| 138 iterable = iterable.toList(); | |
| 139 } | |
| 140 int insertionLength = iterable.length; | |
| 141 // There might be errors after the length change, in which case the list | |
| 142 // will end up being modified but the operation not complete. Unless we | |
| 143 // always go through a "toList" we can't really avoid that. | |
| 144 int len = _list.length; | |
| 145 _list.length += insertionLength; | |
| 146 | |
| 147 _list.setRange(index + insertionLength, this.length, this, index); | |
| 148 _list.setAll(index, iterable); | |
| 149 | |
| 150 if (hasObservers && insertionLength > 0) { | |
| 151 _recordChange(new ListChangeRecord(index, addedCount: insertionLength)); | |
| 152 } | |
| 153 } | |
| 154 | |
| 155 void insert(int index, E element) { | |
| 156 if (index < 0 || index > length) { | |
| 157 throw new RangeError.range(index, 0, length); | |
| 158 } | |
| 159 if (index == length) { | |
| 160 add(element); | |
| 161 return; | |
| 162 } | |
| 163 // We are modifying the length just below the is-check. Without the check | |
| 164 // Array.copy could throw an exception, leaving the list in a bad state | |
| 165 // (with a length that has been increased, but without a new element). | |
| 166 if (index is! int) throw new ArgumentError(index); | |
| 167 _list.length++; | |
| 168 _list.setRange(index + 1, length, this, index); | |
| 169 if (hasObservers) { | |
| 170 _recordChange(new ListChangeRecord(index, addedCount: 1)); | |
| 171 } | |
| 172 _list[index] = element; | |
| 173 } | |
| 174 | |
| 175 | |
| 176 E removeAt(int index) { | |
| 177 E result = this[index]; | |
| 178 removeRange(index, index + 1); | |
| 179 return result; | |
| 180 } | |
| 181 | |
| 182 void _rangeCheck(int start, int end) { | |
| 183 if (start < 0 || start > this.length) { | |
| 184 throw new RangeError.range(start, 0, this.length); | |
| 185 } | |
| 186 if (end < start || end > this.length) { | |
| 187 throw new RangeError.range(end, start, this.length); | |
| 188 } | |
| 189 } | |
| 190 | |
| 191 void _recordChange(ListChangeRecord record) { | |
| 192 if (_records == null) { | |
| 193 _records = []; | |
| 194 queueChangeRecords(_summarizeRecords); | |
| 195 } | |
| 196 _records.add(record); | |
| 197 } | |
| 198 | |
| 199 /** | |
| 200 * We need to summarize change records. Consumers of these records want to | |
| 201 * apply the batch sequentially, and ensure that they can find inserted | |
| 202 * items by looking at that position in the list. This property does not | |
| 203 * hold in our record-as-you-go records. Consider: | |
| 204 * | |
| 205 * var model = toObservable(['a', 'b']); | |
| 206 * model.removeAt(1); | |
| 207 * model.insertAll(0, ['c', 'd', 'e']); | |
| 208 * model.removeRange(1, 3); | |
| 209 * model.insert(1, 'f'); | |
| 210 * | |
| 211 * Here, we inserted some records and then removed some of them. | |
| 212 * If someone processed these records naively, they would "play back" the | |
| 213 * insert incorrectly, because those items will be shifted. | |
| 214 * | |
| 215 * We summarize changes using a straightforward technique: | |
| 216 * Simulate the moves and use the final item positions to synthesize a | |
| 217 * new list of changes records. This has the advantage of not depending | |
| 218 * on the actual *values*, so we don't need to perform N^2 edit | |
| 219 */ | |
| 220 // TODO(jmesserly): there's probably something smarter here, but this | |
| 221 // algorithm is pretty simple. It has complexity equivalent to the original | |
| 222 // list modifications. | |
| 223 // One simple idea: we can simply update the index map as we do the operations | |
| 224 // to the list, then produce the records at the end. | |
| 225 void _summarizeRecords() { | |
| 226 int oldLength = length; | |
| 227 for (var r in _records) { | |
| 228 oldLength += r.removedCount - r.addedCount; | |
| 229 } | |
| 230 | |
| 231 if (length != oldLength) { | |
| 232 notifyPropertyChange(_LENGTH, oldLength, length); | |
| 233 } | |
| 234 | |
| 235 if (_records.length == 1) { | |
| 236 notifyChange(_records[0]); | |
| 237 _records = null; | |
| 238 return; | |
| 239 } | |
| 240 | |
| 241 var items = []; | |
| 242 for (int i = 0; i < oldLength; i++) items.add(i); | |
| 243 for (var r in _records) { | |
| 244 items.removeRange(r.index, r.index + r.removedCount); | |
| 245 | |
| 246 // Represent inserts with -1. | |
| 247 items.insertAll(r.index, new List.filled(r.addedCount, -1)); | |
| 248 } | |
| 249 assert(items.length == length); | |
| 250 | |
| 251 _records = null; | |
| 252 | |
| 253 int index = 0; | |
| 254 int offset = 0; | |
| 255 while (index < items.length) { | |
| 256 // Skip unchanged items. | |
| 257 while (index < items.length && items[index] == index + offset) { | |
| 258 index++; | |
| 259 } | |
| 260 | |
| 261 // Find inserts | |
| 262 int startIndex = index; | |
| 263 while (index < items.length && items[index] == -1) { | |
| 264 index++; | |
| 265 } | |
| 266 | |
| 267 int added = index - startIndex; | |
| 268 | |
| 269 // Use the delta between our actual and expected position to determine | |
| 270 // how much was removed. | |
| 271 int actualItem = index < items.length ? items[index] : oldLength; | |
| 272 int expectedItem = startIndex + offset; | |
| 273 | |
| 274 int removed = actualItem - expectedItem; | |
| 275 | |
| 276 if (added > 0 || removed > 0) { | |
| 277 notifyChange(new ListChangeRecord(startIndex, addedCount: added, | |
| 278 removedCount: removed)); | |
| 279 } | |
| 280 | |
| 281 offset += removed - added; | |
| 282 } | |
| 283 } | |
| 284 } | |
| 285 | |
| 286 // TODO(jmesserly): bogus type to workaround spurious VM bug with generic base | |
| 287 // class and mixins. | |
| 288 abstract class _ListBaseWorkaround extends ListBase<dynamic> {} | |
| OLD | NEW |