OLD | NEW |
| (Empty) |
1 // Copyright (c) 2011, 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('observable'); | |
6 | |
7 #import('dart:coreimpl'); | |
8 | |
9 #source('ChangeEvent.dart'); | |
10 #source('EventBatch.dart'); | |
11 | |
12 /** | |
13 * An object whose changes are tracked and who can issue events notifying how it | |
14 * has been changed. | |
15 */ | |
16 interface Observable { | |
17 /** Returns a globally unique identifier for the object. */ | |
18 // TODO(sigmund): remove once dart supports maps with arbitrary keys. | |
19 final int uid; | |
20 | |
21 /** Listeners on this model. */ | |
22 final List<ChangeListener> listeners; | |
23 | |
24 /** The parent observable to notify when this child is changed. */ | |
25 final Observable parent; | |
26 | |
27 /** | |
28 * Adds a listener for changes on this observable instance. Returns whether | |
29 * the listener was added successfully. | |
30 */ | |
31 bool addChangeListener(ChangeListener listener); | |
32 | |
33 /** | |
34 * Removes a listener for changes on this observable instance. Returns whether | |
35 * the listener was removed successfully. | |
36 */ | |
37 bool removeChangeListener(ChangeListener listener); | |
38 } | |
39 | |
40 | |
41 /** Common functionality for observable objects. */ | |
42 class AbstractObservable implements Observable { | |
43 | |
44 /** Unique id to identify this model in an event batch. */ | |
45 final int uid; | |
46 | |
47 /** The parent observable to notify when this child is changed. */ | |
48 final Observable parent; | |
49 | |
50 /** Listeners on this model. */ | |
51 List<ChangeListener> listeners; | |
52 | |
53 /** Whether this object is currently observed by listeners or propagators. */ | |
54 bool get isObserved() { | |
55 for (Observable obj = this; obj != null; obj = obj.parent) { | |
56 if (listeners.length > 0) { | |
57 return true; | |
58 } | |
59 } | |
60 return false; | |
61 } | |
62 | |
63 AbstractObservable([Observable this.parent = null]) | |
64 : uid = EventBatch.genUid(), | |
65 listeners = new List<ChangeListener>(); | |
66 | |
67 bool addChangeListener(ChangeListener listener) { | |
68 if (listeners.indexOf(listener, 0) == -1) { | |
69 listeners.add(listener); | |
70 return true; | |
71 } | |
72 | |
73 return false; | |
74 } | |
75 | |
76 bool removeChangeListener(ChangeListener listener) { | |
77 // TODO(rnystrom): This is awkward without List.remove(e). | |
78 if (listeners.indexOf(listener, 0) != -1) { | |
79 bool found = false; | |
80 listeners = listeners.filter((e) => found || !(found = (e == listener))); | |
81 return true; | |
82 } else { | |
83 return false; | |
84 } | |
85 } | |
86 | |
87 void recordPropertyUpdate(String propertyName, newValue, oldValue) { | |
88 recordEvent(new ChangeEvent.property( | |
89 this, propertyName, newValue, oldValue)); | |
90 } | |
91 | |
92 void recordListUpdate(int index, newValue, oldValue) { | |
93 recordEvent(new ChangeEvent.list( | |
94 this, ChangeEvent.UPDATE, index, newValue, oldValue)); | |
95 } | |
96 | |
97 void recordListInsert(int index, newValue) { | |
98 recordEvent(new ChangeEvent.list( | |
99 this, ChangeEvent.INSERT, index, newValue, null)); | |
100 } | |
101 | |
102 void recordListRemove(int index, oldValue) { | |
103 recordEvent(new ChangeEvent.list( | |
104 this, ChangeEvent.REMOVE, index, null, oldValue)); | |
105 } | |
106 | |
107 void recordGlobalChange() { | |
108 recordEvent(new ChangeEvent.global(this)); | |
109 } | |
110 | |
111 void recordEvent(ChangeEvent event) { | |
112 // Bail if no one cares about the event. | |
113 if (!isObserved) { | |
114 return; | |
115 } | |
116 | |
117 if (EventBatch.current != null) { | |
118 // Already in a batch, so just add it. | |
119 assert (!EventBatch.current.sealed); | |
120 // TODO(sigmund): measure the performance implications of this indirection | |
121 // and consider whether caching the summary object in this instance helps. | |
122 var summary = EventBatch.current.getEvents(this); | |
123 summary.addEvent(event); | |
124 } else { | |
125 // Not in a batch, so create a one-off one. | |
126 // TODO(rnystrom): Needing to do ignore and (null) here is awkward. | |
127 EventBatch.wrap((ignore) { recordEvent(event); })(null); | |
128 } | |
129 } | |
130 } | |
131 | |
132 /** A growable list that fires events when it's modified. */ | |
133 class ObservableList<T> | |
134 extends AbstractObservable | |
135 implements List<T>, Observable { | |
136 | |
137 /** Underlying list. */ | |
138 // TODO(rnystrom): Make this final if we get list.remove(). | |
139 List<T> _internal; | |
140 | |
141 ObservableList([Observable parent = null]) | |
142 : super(parent), _internal = new List<T>(); | |
143 | |
144 T operator [](int index) => _internal[index]; | |
145 | |
146 void operator []=(int index, T value) { | |
147 recordListUpdate(index, value, _internal[index]); | |
148 _internal[index] = value; | |
149 } | |
150 | |
151 int get length() => _internal.length; | |
152 | |
153 void set length(int value) { | |
154 _internal.length = value; | |
155 recordGlobalChange(); | |
156 } | |
157 | |
158 void clear() { | |
159 _internal.clear(); | |
160 recordGlobalChange(); | |
161 } | |
162 | |
163 void sort(int compare(Object a, Object b)) { | |
164 _internal.sort(compare); | |
165 recordGlobalChange(); | |
166 } | |
167 | |
168 void add(T element) { | |
169 recordListInsert(length, element); | |
170 _internal.add(element); | |
171 } | |
172 | |
173 void addLast(T element) { | |
174 add(element); | |
175 } | |
176 | |
177 void addAll(Collection<T> elements) { | |
178 for (T element in elements) { | |
179 add(element); | |
180 } | |
181 } | |
182 | |
183 int push(T element) { | |
184 recordListInsert(length, element); | |
185 _internal.add(element); | |
186 return _internal.length; | |
187 } | |
188 | |
189 T last() => _internal.last(); | |
190 | |
191 T removeLast() { | |
192 final result = _internal.removeLast(); | |
193 recordListRemove(length, result); | |
194 return result; | |
195 } | |
196 | |
197 T removeAt(int index) { | |
198 int i = 0; | |
199 T found = null; | |
200 _internal = _internal.filter(bool _(element) { | |
201 if (i++ == index) { | |
202 found = element; | |
203 return false; | |
204 } | |
205 return true; | |
206 }); | |
207 if (found != null) { | |
208 recordListRemove(index, found); | |
209 } | |
210 return found; | |
211 } | |
212 | |
213 int indexOf(T element, [int start = 0]) { | |
214 return _internal.indexOf(element, start); | |
215 } | |
216 | |
217 int lastIndexOf(T element, [int start = null]) { | |
218 if (start === null) start = length - 1; | |
219 return _internal.lastIndexOf(element, start); | |
220 } | |
221 | |
222 bool removeFirstElement(T element) { | |
223 // the removeAt above will record the event. | |
224 return (removeAt(indexOf(element, 0)) != null); | |
225 } | |
226 | |
227 int removeAllElements(T element) { | |
228 int count = 0; | |
229 for (int i = 0; i < length; i++) { | |
230 if (_internal[i] == element) { | |
231 // the removeAt above will record the event. | |
232 removeAt(i); | |
233 // adjust index since remove shifted elements. | |
234 i--; | |
235 count++; | |
236 } | |
237 } | |
238 return count; | |
239 } | |
240 | |
241 void copyFrom(List<T> src, int srcStart, int dstStart, int count) { | |
242 Arrays.copy(src, srcStart, this, dstStart, count); | |
243 } | |
244 | |
245 void setRange(int start, int length, List from, [int startFrom = 0]) { | |
246 throw const NotImplementedException(); | |
247 } | |
248 | |
249 void removeRange(int start, int length) { | |
250 throw const NotImplementedException(); | |
251 } | |
252 | |
253 void insertRange(int start, int length, [initialValue = null]) { | |
254 throw const NotImplementedException(); | |
255 } | |
256 | |
257 List getRange(int start, int length) { | |
258 throw const NotImplementedException(); | |
259 } | |
260 | |
261 // Iterable<T>: | |
262 Iterator<T> iterator() => _internal.iterator(); | |
263 | |
264 // Collection<T>: | |
265 Collection<T> filter(bool f(T element)) => _internal.filter(f); | |
266 Collection map(f(T element)) => _internal.map(f); | |
267 bool every(bool f(T element)) => _internal.every(f); | |
268 bool some(bool f(T element)) => _internal.some(f); | |
269 void forEach(void f(T element)) { _internal.forEach(f); } | |
270 bool isEmpty() => length == 0; | |
271 } | |
272 | |
273 // TODO(jmesserly): is this too granular? Other similar systems make whole | |
274 // classes observable instead of individual fields. The memory cost of having | |
275 // every field effectively boxed, plus having a listeners list is likely too | |
276 // much. Also, making a value observable necessitates adding ".value" to lots | |
277 // of places, and constructing all fields with the verbose | |
278 // "new ObservableValue<DataType>(myValue)". | |
279 /** A wrapper around a single value whose change can be observed. */ | |
280 class ObservableValue<T> extends AbstractObservable { | |
281 ObservableValue(T value, [Observable parent = null]) | |
282 : super(parent), _value = value; | |
283 | |
284 T get value() => _value; | |
285 | |
286 void set value(T newValue) { | |
287 // Only fire on an actual change. | |
288 // TODO(terry): An object identity test === is needed. Each DataSource has | |
289 // its own operator == which does a value compare. Which | |
290 // equality check should be done? | |
291 if (newValue !== _value) { | |
292 final oldValue = _value; | |
293 _value = newValue; | |
294 recordPropertyUpdate("value", newValue, oldValue); | |
295 } | |
296 } | |
297 | |
298 T _value; | |
299 } | |
OLD | NEW |