| // Copyright (c) 2011, 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. |
| |
| // @dart = 2.9 |
| |
| library observable; |
| |
| part 'ChangeEvent.dart'; |
| part 'EventBatch.dart'; |
| |
| /** |
| * An object whose changes are tracked and who can issue events notifying how it |
| * has been changed. |
| */ |
| abstract class Observable { |
| /** Returns a globally unique identifier for the object. */ |
| // TODO(sigmund): remove once dart supports maps with arbitrary keys. |
| int get uid; |
| |
| /** Listeners on this model. */ |
| List<ChangeListener> get listeners; |
| |
| /** The parent observable to notify when this child is changed. */ |
| Observable get parent; |
| |
| /** |
| * Adds a listener for changes on this observable instance. Returns whether |
| * the listener was added successfully. |
| */ |
| bool addChangeListener(ChangeListener listener); |
| |
| /** |
| * Removes a listener for changes on this observable instance. Returns whether |
| * the listener was removed successfully. |
| */ |
| bool removeChangeListener(ChangeListener listener); |
| } |
| |
| /** Common functionality for observable objects. */ |
| class AbstractObservable implements Observable { |
| /** Unique id to identify this model in an event batch. */ |
| final int uid; |
| |
| /** The parent observable to notify when this child is changed. */ |
| final Observable parent; |
| |
| /** Listeners on this model. */ |
| List<ChangeListener> listeners; |
| |
| /** Whether this object is currently observed by listeners or propagators. */ |
| bool get isObserved { |
| for (Observable obj = this; obj != null; obj = obj.parent) { |
| if (listeners.length > 0) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| AbstractObservable([Observable this.parent = null]) |
| : uid = EventBatch.genUid(), |
| listeners = new List<ChangeListener>(); |
| |
| bool addChangeListener(ChangeListener listener) { |
| if (listeners.indexOf(listener, 0) == -1) { |
| listeners.add(listener); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| bool removeChangeListener(ChangeListener listener) { |
| var index = listeners.indexOf(listener, 0); |
| if (index != -1) { |
| listeners.removeAt(index); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| |
| void recordPropertyUpdate(String propertyName, newValue, oldValue) { |
| recordEvent( |
| new ChangeEvent.property(this, propertyName, newValue, oldValue)); |
| } |
| |
| void recordListUpdate(int index, newValue, oldValue) { |
| recordEvent(new ChangeEvent.list( |
| this, ChangeEvent.UPDATE, index, newValue, oldValue)); |
| } |
| |
| void recordListInsert(int index, newValue) { |
| recordEvent( |
| new ChangeEvent.list(this, ChangeEvent.INSERT, index, newValue, null)); |
| } |
| |
| void recordListRemove(int index, oldValue) { |
| recordEvent( |
| new ChangeEvent.list(this, ChangeEvent.REMOVE, index, null, oldValue)); |
| } |
| |
| void recordGlobalChange() { |
| recordEvent(new ChangeEvent.global(this)); |
| } |
| |
| void recordEvent(ChangeEvent event) { |
| // Bail if no one cares about the event. |
| if (!isObserved) { |
| return; |
| } |
| |
| if (EventBatch.current != null) { |
| // Already in a batch, so just add it. |
| assert(!EventBatch.current.sealed); |
| // TODO(sigmund): measure the performance implications of this indirection |
| // and consider whether caching the summary object in this instance helps. |
| var summary = EventBatch.current.getEvents(this); |
| summary.addEvent(event); |
| } else { |
| // Not in a batch, so create a one-off one. |
| // TODO(rnystrom): Needing to do ignore and (null) here is awkward. |
| EventBatch.wrap((ignore) { |
| recordEvent(event); |
| })(null); |
| } |
| } |
| } |
| |
| /** A growable list that fires events when it's modified. */ |
| class ObservableList<T> extends AbstractObservable |
| implements List<T>, Observable { |
| /** Underlying list. */ |
| // TODO(rnystrom): Make this final if we get list.remove(). |
| List<T> _internal; |
| |
| ObservableList([Observable parent = null]) |
| : _internal = new List<T>(), |
| super(parent); |
| |
| T operator [](int index) => _internal[index]; |
| |
| void operator []=(int index, T value) { |
| recordListUpdate(index, value, _internal[index]); |
| _internal[index] = value; |
| } |
| |
| int get length => _internal.length; |
| |
| List<R> cast<R>() => _internal.cast<R>(); |
| Iterable<R> whereType<R>() => _internal.whereType<R>(); |
| |
| List<T> operator +(List<T> other) => _internal + other; |
| |
| Iterable<T> followedBy(Iterable<T> other) => _internal.followedBy(other); |
| |
| int indexWhere(bool test(T element), [int start = 0]) => |
| _internal.indexWhere(test, start); |
| |
| int lastIndexWhere(bool test(T element), [int start]) => |
| _internal.lastIndexWhere(test, start); |
| |
| void set length(int value) { |
| _internal.length = value; |
| recordGlobalChange(); |
| } |
| |
| void clear() { |
| _internal.clear(); |
| recordGlobalChange(); |
| } |
| |
| Iterable<T> get reversed => _internal.reversed; |
| |
| void sort([int compare(T a, T b)]) { |
| //if (compare == null) compare = (u, v) => Comparable.compare(u, v); |
| _internal.sort(compare); |
| recordGlobalChange(); |
| } |
| |
| void add(T element) { |
| recordListInsert(length, element); |
| _internal.add(element); |
| } |
| |
| void addAll(Iterable<T> elements) { |
| for (T element in elements) { |
| add(element); |
| } |
| } |
| |
| int push(T element) { |
| recordListInsert(length, element); |
| _internal.add(element); |
| return _internal.length; |
| } |
| |
| T get first => _internal.first; |
| void set first(T value) { |
| _internal.first = value; |
| } |
| |
| T get last => _internal.last; |
| void set last(T value) { |
| _internal.last = value; |
| } |
| |
| T get single => _internal.single; |
| |
| void insert(int index, T element) { |
| _internal.insert(index, element); |
| recordListInsert(index, element); |
| } |
| |
| void insertAll(int index, Iterable<T> iterable) { |
| throw new UnimplementedError(); |
| } |
| |
| void setAll(int index, Iterable<T> iterable) { |
| throw new UnimplementedError(); |
| } |
| |
| T removeLast() { |
| final result = _internal.removeLast(); |
| recordListRemove(length, result); |
| return result; |
| } |
| |
| T removeAt(int index) { |
| T result = _internal.removeAt(index); |
| recordListRemove(index, result); |
| return result; |
| } |
| |
| int indexOf(Object element, [int start = 0]) { |
| return _internal.indexOf(element, start); |
| } |
| |
| int lastIndexOf(Object element, [int start]) { |
| if (start == null) start = length - 1; |
| return _internal.lastIndexOf(element, start); |
| } |
| |
| bool removeFirstElement(T element) { |
| // the removeAt above will record the event. |
| return (removeAt(indexOf(element, 0)) != null); |
| } |
| |
| int removeAllElements(T element) { |
| int count = 0; |
| for (int i = 0; i < length; i++) { |
| if (_internal[i] == element) { |
| // the removeAt above will record the event. |
| removeAt(i); |
| // adjust index since remove shifted elements. |
| i--; |
| count++; |
| } |
| } |
| return count; |
| } |
| |
| void copyFrom(List<T> src, int srcStart, int dstStart, int count) { |
| List dst = this; |
| if (srcStart == null) srcStart = 0; |
| if (dstStart == null) dstStart = 0; |
| |
| if (srcStart < dstStart) { |
| for (int i = srcStart + count - 1, j = dstStart + count - 1; |
| i >= srcStart; |
| i--, j--) { |
| dst[j] = src[i]; |
| } |
| } else { |
| for (int i = srcStart, j = dstStart; i < srcStart + count; i++, j++) { |
| dst[j] = src[i]; |
| } |
| } |
| } |
| |
| void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) { |
| throw new UnimplementedError(); |
| } |
| |
| void removeRange(int start, int end) { |
| throw new UnimplementedError(); |
| } |
| |
| void replaceRange(int start, int end, Iterable<T> iterable) { |
| throw new UnimplementedError(); |
| } |
| |
| void fillRange(int start, int end, [T fillValue]) { |
| throw new UnimplementedError(); |
| } |
| |
| List<T> sublist(int start, [int end]) { |
| throw new UnimplementedError(); |
| } |
| |
| Iterable<T> getRange(int start, int end) { |
| throw new UnimplementedError(); |
| } |
| |
| bool contains(Object element) { |
| throw new UnimplementedError(); |
| } |
| |
| T reduce(T combine(T previousValue, T element)) { |
| throw new UnimplementedError(); |
| } |
| |
| R fold<R>(R initialValue, R combine(R previousValue, T element)) { |
| throw new UnimplementedError(); |
| } |
| |
| // Iterable<T>: |
| Iterator<T> get iterator => _internal.iterator; |
| |
| Iterable<T> where(bool f(T element)) => _internal.where(f); |
| Iterable<R> map<R>(R f(T element)) => _internal.map(f); |
| Iterable<R> expand<R>(Iterable<R> f(T element)) => _internal.expand(f); |
| List<T> skip(int count) => _internal.skip(count); |
| List<T> take(int count) => _internal.take(count); |
| bool every(bool f(T element)) => _internal.every(f); |
| bool any(bool f(T element)) => _internal.any(f); |
| void forEach(void f(T element)) { |
| _internal.forEach(f); |
| } |
| |
| String join([String separator = ""]) => _internal.join(separator); |
| T firstWhere(bool test(T value), {T orElse()}) { |
| return _internal.firstWhere(test, orElse: orElse); |
| } |
| |
| T lastWhere(bool test(T value), {T orElse()}) { |
| return _internal.lastWhere(test, orElse: orElse); |
| } |
| |
| void shuffle([random]) => throw new UnimplementedError(); |
| bool remove(Object element) => throw new UnimplementedError(); |
| void removeWhere(bool test(T element)) => throw new UnimplementedError(); |
| void retainWhere(bool test(T element)) => throw new UnimplementedError(); |
| List<T> toList({bool growable: true}) => throw new UnimplementedError(); |
| Set<T> toSet() => throw new UnimplementedError(); |
| Iterable<T> takeWhile(bool test(T value)) => throw new UnimplementedError(); |
| Iterable<T> skipWhile(bool test(T value)) => throw new UnimplementedError(); |
| |
| T singleWhere(bool test(T value), {T orElse()}) { |
| return _internal.singleWhere(test, orElse: orElse); |
| } |
| |
| T elementAt(int index) { |
| return _internal.elementAt(index); |
| } |
| |
| Map<int, T> asMap() { |
| return _internal.asMap(); |
| } |
| |
| bool get isEmpty => length == 0; |
| |
| bool get isNotEmpty => !isEmpty; |
| } |
| |
| // TODO(jmesserly): is this too granular? Other similar systems make whole |
| // classes observable instead of individual fields. The memory cost of having |
| // every field effectively boxed, plus having a listeners list is likely too |
| // much. Also, making a value observable necessitates adding ".value" to lots |
| // of places, and constructing all fields with the verbose |
| // "new ObservableValue<DataType>(myValue)". |
| /** A wrapper around a single value whose change can be observed. */ |
| class ObservableValue<T> extends AbstractObservable { |
| ObservableValue(T value, [Observable parent = null]) |
| : _value = value, |
| super(parent); |
| |
| T get value => _value; |
| |
| void set value(T newValue) { |
| // Only fire on an actual change. |
| if (!identical(newValue, _value)) { |
| final oldValue = _value; |
| _value = newValue; |
| recordPropertyUpdate("value", newValue, oldValue); |
| } |
| } |
| |
| T _value; |
| } |