OLD | NEW |
1 // Copyright (c) 2013, the Dart project authors. Please see the AUTHORS file | 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 | 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. | 3 // BSD-style license that can be found in the LICENSE file. |
4 | 4 |
5 library observe.src.path_observer; | 5 library observe.src.path_observer; |
6 | 6 |
7 import 'dart:async'; | 7 import 'dart:async'; |
8 import 'dart:collection'; | 8 import 'dart:collection'; |
9 import 'dart:math' show min; | 9 import 'dart:math' show min; |
10 @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty], | 10 @MirrorsUsed(metaTargets: const [Reflectable, ObservableProperty], |
11 override: 'observe.src.path_observer') | 11 override: 'smoke.mirrors') |
12 import 'dart:mirrors'; | 12 import 'dart:mirrors' show MirrorsUsed; |
| 13 |
13 import 'package:logging/logging.dart' show Logger, Level; | 14 import 'package:logging/logging.dart' show Logger, Level; |
14 import 'package:observe/observe.dart'; | 15 import 'package:observe/observe.dart'; |
15 import 'package:observe/src/observable.dart' show objectType; | 16 import 'package:observe/src/observable.dart' show objectType; |
| 17 import 'package:smoke/smoke.dart' as smoke; |
16 | 18 |
17 /// A data-bound path starting from a view-model or model object, for example | 19 /// A data-bound path starting from a view-model or model object, for example |
18 /// `foo.bar.baz`. | 20 /// `foo.bar.baz`. |
19 /// | 21 /// |
20 /// When [open] is called, this will observe changes to the object and any | 22 /// When [open] is called, this will observe changes to the object and any |
21 /// intermediate object along the path, and send updated values accordingly. | 23 /// intermediate object along the path, and send updated values accordingly. |
22 /// When [close] is called it will stop observing the objects. | 24 /// When [close] is called it will stop observing the objects. |
23 /// | 25 /// |
24 /// This class is used to implement `Node.bind` and similar functionality in | 26 /// This class is used to implement `Node.bind` and similar functionality in |
25 /// the [template_binding](pub.dartlang.org/packages/template_binding) package. | 27 /// the [template_binding](pub.dartlang.org/packages/template_binding) package. |
26 class PathObserver extends _Observer implements Bindable { | 28 class PathObserver extends _Observer implements Bindable { |
27 PropertyPath _path; | 29 PropertyPath _path; |
28 Object _object; | 30 Object _object; |
29 _ObservedSet _directObserver; | 31 _ObservedSet _directObserver; |
30 | 32 |
31 /// Observes [path] on [object] for changes. This returns an object | 33 /// Observes [path] on [object] for changes. This returns an object |
32 /// that can be used to get the changes and get/set the value at this path. | 34 /// that can be used to get the changes and get/set the value at this path. |
33 /// | 35 /// |
34 /// The path can be a [PropertyPath], or a [String] used to construct it. | 36 /// The path can be a [PropertyPath], or a [String] used to construct it. |
35 /// | 37 /// |
36 /// See [open] and [value]. | 38 /// See [open] and [value]. |
37 PathObserver(Object object, [path]) | 39 PathObserver(Object object, [path]) |
38 : _object = object, | 40 : _object = object, |
39 _path = path is PropertyPath ? path : new PropertyPath(path); | 41 _path = path is PropertyPath ? path : new PropertyPath(path); |
40 | 42 |
41 bool get _isClosed => _path == null; | 43 bool get _isClosed => _path == null; |
42 | 44 |
43 /// Sets the value at this path. | 45 /// Sets the value at this path. |
44 @reflectable void set value(Object newValue) { | 46 void set value(Object newValue) { |
45 if (_path != null) _path.setValueFrom(_object, newValue); | 47 if (_path != null) _path.setValueFrom(_object, newValue); |
46 } | 48 } |
47 | 49 |
48 int get _reportArgumentCount => 2; | 50 int get _reportArgumentCount => 2; |
49 | 51 |
50 /// Initiates observation and returns the initial value. | 52 /// Initiates observation and returns the initial value. |
51 /// The callback will be passed the updated [value], and may optionally be | 53 /// The callback will be passed the updated [value], and may optionally be |
52 /// declared to take a second argument, which will contain the previous value. | 54 /// declared to take a second argument, which will contain the previous value. |
53 open(callback) => super.open(callback); | 55 open(callback) => super.open(callback); |
54 | 56 |
(...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
141 | 143 |
142 PropertyPath._(this._segments); | 144 PropertyPath._(this._segments); |
143 | 145 |
144 int get length => _segments.length; | 146 int get length => _segments.length; |
145 bool get isEmpty => _segments.isEmpty; | 147 bool get isEmpty => _segments.isEmpty; |
146 bool get isValid => true; | 148 bool get isValid => true; |
147 | 149 |
148 String toString() { | 150 String toString() { |
149 if (!isValid) return '<invalid path>'; | 151 if (!isValid) return '<invalid path>'; |
150 return _segments | 152 return _segments |
151 .map((s) => s is Symbol ? MirrorSystem.getName(s) : s) | 153 .map((s) => s is Symbol ? smoke.symbolToName(s) : s) |
152 .join('.'); | 154 .join('.'); |
153 } | 155 } |
154 | 156 |
155 bool operator ==(other) { | 157 bool operator ==(other) { |
156 if (identical(this, other)) return true; | 158 if (identical(this, other)) return true; |
157 if (other is! PropertyPath) return false; | 159 if (other is! PropertyPath) return false; |
158 if (isValid != other.isValid) return false; | 160 if (isValid != other.isValid) return false; |
159 | 161 |
160 int len = _segments.length; | 162 int len = _segments.length; |
161 if (len != other._segments.length) return false; | 163 if (len != other._segments.length) return false; |
(...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
222 | 224 |
223 bool get isValid => false; | 225 bool get isValid => false; |
224 _InvalidPropertyPath() : super._([]); | 226 _InvalidPropertyPath() : super._([]); |
225 } | 227 } |
226 | 228 |
227 bool _changeRecordMatches(record, key) { | 229 bool _changeRecordMatches(record, key) { |
228 if (record is PropertyChangeRecord) { | 230 if (record is PropertyChangeRecord) { |
229 return (record as PropertyChangeRecord).name == key; | 231 return (record as PropertyChangeRecord).name == key; |
230 } | 232 } |
231 if (record is MapChangeRecord) { | 233 if (record is MapChangeRecord) { |
232 if (key is Symbol) key = MirrorSystem.getName(key); | 234 if (key is Symbol) key = smoke.symbolToName(key); |
233 return (record as MapChangeRecord).key == key; | 235 return (record as MapChangeRecord).key == key; |
234 } | 236 } |
235 return false; | 237 return false; |
236 } | 238 } |
237 | 239 |
238 _getObjectProperty(object, property) { | 240 _getObjectProperty(object, property) { |
239 if (object == null) return null; | 241 if (object == null) return null; |
240 | 242 |
241 if (property is int) { | 243 if (property is int) { |
242 if (object is List && property >= 0 && property < object.length) { | 244 if (object is List && property >= 0 && property < object.length) { |
243 return object[property]; | 245 return object[property]; |
244 } | 246 } |
245 } else if (property is Symbol) { | 247 } else if (property is Symbol) { |
246 var mirror = reflect(object); | 248 final type = object.runtimeType; |
247 final type = mirror.type; | |
248 try { | 249 try { |
249 if (_maybeHasGetter(type, property)) { | 250 if (smoke.hasGetter(type, property) || smoke.hasNoSuchMethod(type)) { |
250 return mirror.getField(property).reflectee; | 251 return smoke.read(object, property); |
251 } | 252 } |
252 // Support indexer if available, e.g. Maps or polymer_expressions Scope. | 253 // Support indexer if available, e.g. Maps or polymer_expressions Scope. |
253 // This is the default syntax used by polymer/nodebind and | 254 // This is the default syntax used by polymer/nodebind and |
254 // polymer/observe-js PathObserver. | 255 // polymer/observe-js PathObserver. |
255 if (_hasMethod(type, const Symbol('[]'))) { | 256 if (smoke.hasInstanceMethod(type, const Symbol('[]'))) { |
256 return object[MirrorSystem.getName(property)]; | 257 return object[smoke.symbolToName(property)]; |
257 } | 258 } |
258 } on NoSuchMethodError catch (e) { | 259 } on NoSuchMethodError catch (e) { |
259 // Rethrow, unless the type implements noSuchMethod, in which case we | 260 // Rethrow, unless the type implements noSuchMethod, in which case we |
260 // interpret the exception as a signal that the method was not found. | 261 // interpret the exception as a signal that the method was not found. |
261 if (!_hasMethod(type, #noSuchMethod)) rethrow; | 262 if (!smoke.hasNoSuchMethod(type)) rethrow; |
262 } | 263 } |
263 } | 264 } |
264 | 265 |
265 if (_logger.isLoggable(Level.FINER)) { | 266 if (_logger.isLoggable(Level.FINER)) { |
266 _logger.finer("can't get $property in $object"); | 267 _logger.finer("can't get $property in $object"); |
267 } | 268 } |
268 return null; | 269 return null; |
269 } | 270 } |
270 | 271 |
271 bool _setObjectProperty(object, property, value) { | 272 bool _setObjectProperty(object, property, value) { |
272 if (object == null) return false; | 273 if (object == null) return false; |
273 | 274 |
274 if (property is int) { | 275 if (property is int) { |
275 if (object is List && property >= 0 && property < object.length) { | 276 if (object is List && property >= 0 && property < object.length) { |
276 object[property] = value; | 277 object[property] = value; |
277 return true; | 278 return true; |
278 } | 279 } |
279 } else if (property is Symbol) { | 280 } else if (property is Symbol) { |
280 var mirror = reflect(object); | 281 final type = object.runtimeType; |
281 final type = mirror.type; | |
282 try { | 282 try { |
283 if (_maybeHasSetter(type, property)) { | 283 if (smoke.hasSetter(type, property) || smoke.hasNoSuchMethod(type)) { |
284 mirror.setField(property, value); | 284 smoke.write(object, property, value); |
285 return true; | 285 return true; |
286 } | 286 } |
287 // Support indexer if available, e.g. Maps or polymer_expressions Scope. | 287 // Support indexer if available, e.g. Maps or polymer_expressions Scope. |
288 if (_hasMethod(type, const Symbol('[]='))) { | 288 if (smoke.hasInstanceMethod(type, const Symbol('[]='))) { |
289 object[MirrorSystem.getName(property)] = value; | 289 object[smoke.symbolToName(property)] = value; |
290 return true; | 290 return true; |
291 } | 291 } |
292 } on NoSuchMethodError catch (e) { | 292 } on NoSuchMethodError catch (e) { |
293 if (!_hasMethod(type, #noSuchMethod)) rethrow; | 293 if (!smoke.hasNoSuchMethod(type)) rethrow; |
294 } | 294 } |
295 } | 295 } |
296 | 296 |
297 if (_logger.isLoggable(Level.FINER)) { | 297 if (_logger.isLoggable(Level.FINER)) { |
298 _logger.finer("can't set $property in $object"); | 298 _logger.finer("can't set $property in $object"); |
299 } | 299 } |
300 return false; | 300 return false; |
301 } | 301 } |
302 | 302 |
303 bool _maybeHasGetter(ClassMirror type, Symbol name) { | |
304 while (type != objectType) { | |
305 final members = type.declarations; | |
306 if (members.containsKey(name)) return true; | |
307 if (members.containsKey(#noSuchMethod)) return true; | |
308 type = _safeSuperclass(type); | |
309 } | |
310 return false; | |
311 } | |
312 | |
313 // TODO(jmesserly): workaround for: | |
314 // https://code.google.com/p/dart/issues/detail?id=10029 | |
315 Symbol _setterName(Symbol getter) => | |
316 new Symbol('${MirrorSystem.getName(getter)}='); | |
317 | |
318 bool _maybeHasSetter(ClassMirror type, Symbol name) { | |
319 var setterName = _setterName(name); | |
320 while (type != objectType) { | |
321 final members = type.declarations; | |
322 if (members[name] is VariableMirror) return true; | |
323 if (members.containsKey(setterName)) return true; | |
324 if (members.containsKey(#noSuchMethod)) return true; | |
325 type = _safeSuperclass(type); | |
326 } | |
327 return false; | |
328 } | |
329 | |
330 /// True if the type has a method, other than on Object. | |
331 /// Doesn't consider noSuchMethod, unless [name] is `#noSuchMethod`. | |
332 bool _hasMethod(ClassMirror type, Symbol name) { | |
333 while (type != objectType) { | |
334 final member = type.declarations[name]; | |
335 if (member is MethodMirror && member.isRegularMethod) return true; | |
336 type = _safeSuperclass(type); | |
337 } | |
338 return false; | |
339 } | |
340 | |
341 ClassMirror _safeSuperclass(ClassMirror type) { | |
342 try { | |
343 return type.superclass; | |
344 } /*on UnsupportedError*/ catch (e) { | |
345 // Note: dart2js throws UnsupportedError when the type is not | |
346 // reflectable. | |
347 // TODO(jmesserly): dart2js also throws a NoSuchMethodError if the `type` is | |
348 // a bound generic, because they are not fully implemented. See | |
349 // https://code.google.com/p/dart/issues/detail?id=15573 | |
350 return objectType; | |
351 } | |
352 } | |
353 | |
354 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js | 303 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
355 | 304 |
356 final _pathRegExp = () { | 305 final _pathRegExp = () { |
357 const identStart = '[\$_a-zA-Z]'; | 306 const identStart = '[\$_a-zA-Z]'; |
358 const identPart = '[\$_a-zA-Z0-9]'; | 307 const identPart = '[\$_a-zA-Z0-9]'; |
359 const ident = '$identStart+$identPart*'; | 308 const ident = '$identStart+$identPart*'; |
360 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; | 309 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; |
361 const identOrElementIndex = '(?:$ident|$elementIndex)'; | 310 const identOrElementIndex = '(?:$ident|$elementIndex)'; |
362 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; | 311 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; |
363 return new RegExp('^$path\$'); | 312 return new RegExp('^$path\$'); |
(...skipping 199 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
563 } | 512 } |
564 | 513 |
565 _notifyCallback = callback; | 514 _notifyCallback = callback; |
566 _notifyArgumentCount = min(_reportArgumentCount, | 515 _notifyArgumentCount = min(_reportArgumentCount, |
567 _maxArgumentCount(callback)); | 516 _maxArgumentCount(callback)); |
568 | 517 |
569 _connect(); | 518 _connect(); |
570 return _value; | 519 return _value; |
571 } | 520 } |
572 | 521 |
573 @reflectable get value { | 522 get value { |
574 _check(skipChanges: true); | 523 _check(skipChanges: true); |
575 return _value; | 524 return _value; |
576 } | 525 } |
577 | 526 |
578 void close() { | 527 void close() { |
579 if (!_isOpen) return; | 528 if (!_isOpen) return; |
580 | 529 |
581 _disconnect(); | 530 _disconnect(); |
582 _value = null; | 531 _value = null; |
583 _notifyCallback = null; | 532 _notifyCallback = null; |
(...skipping 145 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
729 for (var observer in _observers.values.toList(growable: false)) { | 678 for (var observer in _observers.values.toList(growable: false)) { |
730 if (observer._isOpen) observer._check(); | 679 if (observer._isOpen) observer._check(); |
731 } | 680 } |
732 | 681 |
733 _resetNeeded = true; | 682 _resetNeeded = true; |
734 scheduleMicrotask(reset); | 683 scheduleMicrotask(reset); |
735 } | 684 } |
736 } | 685 } |
737 | 686 |
738 const int _MAX_DIRTY_CHECK_CYCLES = 1000; | 687 const int _MAX_DIRTY_CHECK_CYCLES = 1000; |
OLD | NEW |