| 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], | |
| 11 override: 'observe.src.path_observer') | |
| 12 import 'dart:mirrors'; | |
| 13 import 'package:logging/logging.dart' show Logger, Level; | 10 import 'package:logging/logging.dart' show Logger, Level; |
| 14 import 'package:observe/observe.dart'; | 11 import 'package:observe/observe.dart'; |
| 15 import 'package:observe/src/observable.dart' show objectType; | 12 import 'package:observe/src/observable.dart' show objectType; |
| 13 import 'package:smoke/smoke.dart' as smoke; |
| 16 | 14 |
| 17 /// A data-bound path starting from a view-model or model object, for example | 15 /// A data-bound path starting from a view-model or model object, for example |
| 18 /// `foo.bar.baz`. | 16 /// `foo.bar.baz`. |
| 19 /// | 17 /// |
| 20 /// When [open] is called, this will observe changes to the object and any | 18 /// When [open] is called, this will observe changes to the object and any |
| 21 /// intermediate object along the path, and send updated values accordingly. | 19 /// intermediate object along the path, and send updated values accordingly. |
| 22 /// When [close] is called it will stop observing the objects. | 20 /// When [close] is called it will stop observing the objects. |
| 23 /// | 21 /// |
| 24 /// This class is used to implement `Node.bind` and similar functionality in | 22 /// This class is used to implement `Node.bind` and similar functionality in |
| 25 /// the [template_binding](pub.dartlang.org/packages/template_binding) package. | 23 /// the [template_binding](pub.dartlang.org/packages/template_binding) package. |
| 26 class PathObserver extends _Observer implements Bindable { | 24 class PathObserver extends _Observer implements Bindable { |
| 27 PropertyPath _path; | 25 PropertyPath _path; |
| 28 Object _object; | 26 Object _object; |
| 29 _ObservedSet _directObserver; | 27 _ObservedSet _directObserver; |
| 30 | 28 |
| 31 /// Observes [path] on [object] for changes. This returns an object | 29 /// 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. | 30 /// that can be used to get the changes and get/set the value at this path. |
| 33 /// | 31 /// |
| 34 /// The path can be a [PropertyPath], or a [String] used to construct it. | 32 /// The path can be a [PropertyPath], or a [String] used to construct it. |
| 35 /// | 33 /// |
| 36 /// See [open] and [value]. | 34 /// See [open] and [value]. |
| 37 PathObserver(Object object, [path]) | 35 PathObserver(Object object, [path]) |
| 38 : _object = object, | 36 : _object = object, |
| 39 _path = path is PropertyPath ? path : new PropertyPath(path); | 37 _path = path is PropertyPath ? path : new PropertyPath(path); |
| 40 | 38 |
| 41 bool get _isClosed => _path == null; | 39 bool get _isClosed => _path == null; |
| 42 | 40 |
| 43 /// Sets the value at this path. | 41 /// Sets the value at this path. |
| 44 @reflectable void set value(Object newValue) { | 42 void set value(Object newValue) { |
| 45 if (_path != null) _path.setValueFrom(_object, newValue); | 43 if (_path != null) _path.setValueFrom(_object, newValue); |
| 46 } | 44 } |
| 47 | 45 |
| 48 int get _reportArgumentCount => 2; | 46 int get _reportArgumentCount => 2; |
| 49 | 47 |
| 50 /// Initiates observation and returns the initial value. | 48 /// Initiates observation and returns the initial value. |
| 51 /// The callback will be passed the updated [value], and may optionally be | 49 /// 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. | 50 /// declared to take a second argument, which will contain the previous value. |
| 53 open(callback) => super.open(callback); | 51 open(callback) => super.open(callback); |
| 54 | 52 |
| (...skipping 86 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 141 | 139 |
| 142 PropertyPath._(this._segments); | 140 PropertyPath._(this._segments); |
| 143 | 141 |
| 144 int get length => _segments.length; | 142 int get length => _segments.length; |
| 145 bool get isEmpty => _segments.isEmpty; | 143 bool get isEmpty => _segments.isEmpty; |
| 146 bool get isValid => true; | 144 bool get isValid => true; |
| 147 | 145 |
| 148 String toString() { | 146 String toString() { |
| 149 if (!isValid) return '<invalid path>'; | 147 if (!isValid) return '<invalid path>'; |
| 150 return _segments | 148 return _segments |
| 151 .map((s) => s is Symbol ? MirrorSystem.getName(s) : s) | 149 .map((s) => s is Symbol ? smoke.symbolToName(s) : s) |
| 152 .join('.'); | 150 .join('.'); |
| 153 } | 151 } |
| 154 | 152 |
| 155 bool operator ==(other) { | 153 bool operator ==(other) { |
| 156 if (identical(this, other)) return true; | 154 if (identical(this, other)) return true; |
| 157 if (other is! PropertyPath) return false; | 155 if (other is! PropertyPath) return false; |
| 158 if (isValid != other.isValid) return false; | 156 if (isValid != other.isValid) return false; |
| 159 | 157 |
| 160 int len = _segments.length; | 158 int len = _segments.length; |
| 161 if (len != other._segments.length) return false; | 159 if (len != other._segments.length) return false; |
| (...skipping 60 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 222 | 220 |
| 223 bool get isValid => false; | 221 bool get isValid => false; |
| 224 _InvalidPropertyPath() : super._([]); | 222 _InvalidPropertyPath() : super._([]); |
| 225 } | 223 } |
| 226 | 224 |
| 227 bool _changeRecordMatches(record, key) { | 225 bool _changeRecordMatches(record, key) { |
| 228 if (record is PropertyChangeRecord) { | 226 if (record is PropertyChangeRecord) { |
| 229 return (record as PropertyChangeRecord).name == key; | 227 return (record as PropertyChangeRecord).name == key; |
| 230 } | 228 } |
| 231 if (record is MapChangeRecord) { | 229 if (record is MapChangeRecord) { |
| 232 if (key is Symbol) key = MirrorSystem.getName(key); | 230 if (key is Symbol) key = smoke.symbolToName(key); |
| 233 return (record as MapChangeRecord).key == key; | 231 return (record as MapChangeRecord).key == key; |
| 234 } | 232 } |
| 235 return false; | 233 return false; |
| 236 } | 234 } |
| 237 | 235 |
| 238 _getObjectProperty(object, property) { | 236 _getObjectProperty(object, property) { |
| 239 if (object == null) return null; | 237 if (object == null) return null; |
| 240 | 238 |
| 241 if (property is int) { | 239 if (property is int) { |
| 242 if (object is List && property >= 0 && property < object.length) { | 240 if (object is List && property >= 0 && property < object.length) { |
| 243 return object[property]; | 241 return object[property]; |
| 244 } | 242 } |
| 245 } else if (property is Symbol) { | 243 } else if (property is Symbol) { |
| 246 var mirror = reflect(object); | 244 final type = object.runtimeType; |
| 247 final type = mirror.type; | |
| 248 try { | 245 try { |
| 249 if (_maybeHasGetter(type, property)) { | 246 if (smoke.hasGetter(type, property) || smoke.hasNoSuchMethod(type)) { |
| 250 return mirror.getField(property).reflectee; | 247 return smoke.read(object, property); |
| 251 } | 248 } |
| 252 // Support indexer if available, e.g. Maps or polymer_expressions Scope. | 249 // Support indexer if available, e.g. Maps or polymer_expressions Scope. |
| 253 // This is the default syntax used by polymer/nodebind and | 250 // This is the default syntax used by polymer/nodebind and |
| 254 // polymer/observe-js PathObserver. | 251 // polymer/observe-js PathObserver. |
| 255 if (_hasMethod(type, const Symbol('[]'))) { | 252 if (smoke.hasInstanceMethod(type, const Symbol('[]'))) { |
| 256 return object[MirrorSystem.getName(property)]; | 253 return object[smoke.symbolToName(property)]; |
| 257 } | 254 } |
| 258 } on NoSuchMethodError catch (e) { | 255 } on NoSuchMethodError catch (e) { |
| 259 // Rethrow, unless the type implements noSuchMethod, in which case we | 256 // Rethrow, unless the type implements noSuchMethod, in which case we |
| 260 // interpret the exception as a signal that the method was not found. | 257 // interpret the exception as a signal that the method was not found. |
| 261 if (!_hasMethod(type, #noSuchMethod)) rethrow; | 258 if (!smoke.hasNoSuchMethod(type)) rethrow; |
| 262 } | 259 } |
| 263 } | 260 } |
| 264 | 261 |
| 265 if (_logger.isLoggable(Level.FINER)) { | 262 if (_logger.isLoggable(Level.FINER)) { |
| 266 _logger.finer("can't get $property in $object"); | 263 _logger.finer("can't get $property in $object"); |
| 267 } | 264 } |
| 268 return null; | 265 return null; |
| 269 } | 266 } |
| 270 | 267 |
| 271 bool _setObjectProperty(object, property, value) { | 268 bool _setObjectProperty(object, property, value) { |
| 272 if (object == null) return false; | 269 if (object == null) return false; |
| 273 | 270 |
| 274 if (property is int) { | 271 if (property is int) { |
| 275 if (object is List && property >= 0 && property < object.length) { | 272 if (object is List && property >= 0 && property < object.length) { |
| 276 object[property] = value; | 273 object[property] = value; |
| 277 return true; | 274 return true; |
| 278 } | 275 } |
| 279 } else if (property is Symbol) { | 276 } else if (property is Symbol) { |
| 280 var mirror = reflect(object); | 277 final type = object.runtimeType; |
| 281 final type = mirror.type; | |
| 282 try { | 278 try { |
| 283 if (_maybeHasSetter(type, property)) { | 279 if (smoke.hasSetter(type, property) || smoke.hasNoSuchMethod(type)) { |
| 284 mirror.setField(property, value); | 280 smoke.write(object, property, value); |
| 285 return true; | 281 return true; |
| 286 } | 282 } |
| 287 // Support indexer if available, e.g. Maps or polymer_expressions Scope. | 283 // Support indexer if available, e.g. Maps or polymer_expressions Scope. |
| 288 if (_hasMethod(type, const Symbol('[]='))) { | 284 if (smoke.hasInstanceMethod(type, const Symbol('[]='))) { |
| 289 object[MirrorSystem.getName(property)] = value; | 285 object[smoke.symbolToName(property)] = value; |
| 290 return true; | 286 return true; |
| 291 } | 287 } |
| 292 } on NoSuchMethodError catch (e) { | 288 } on NoSuchMethodError catch (e) { |
| 293 if (!_hasMethod(type, #noSuchMethod)) rethrow; | 289 if (!smoke.hasNoSuchMethod(type)) rethrow; |
| 294 } | 290 } |
| 295 } | 291 } |
| 296 | 292 |
| 297 if (_logger.isLoggable(Level.FINER)) { | 293 if (_logger.isLoggable(Level.FINER)) { |
| 298 _logger.finer("can't set $property in $object"); | 294 _logger.finer("can't set $property in $object"); |
| 299 } | 295 } |
| 300 return false; | 296 return false; |
| 301 } | 297 } |
| 302 | 298 |
| 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 | 299 // From: https://github.com/rafaelw/ChangeSummary/blob/master/change_summary.js |
| 355 | 300 |
| 356 final _pathRegExp = () { | 301 final _pathRegExp = () { |
| 357 const identStart = '[\$_a-zA-Z]'; | 302 const identStart = '[\$_a-zA-Z]'; |
| 358 const identPart = '[\$_a-zA-Z0-9]'; | 303 const identPart = '[\$_a-zA-Z0-9]'; |
| 359 const ident = '$identStart+$identPart*'; | 304 const ident = '$identStart+$identPart*'; |
| 360 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; | 305 const elementIndex = '(?:[0-9]|[1-9]+[0-9]+)'; |
| 361 const identOrElementIndex = '(?:$ident|$elementIndex)'; | 306 const identOrElementIndex = '(?:$ident|$elementIndex)'; |
| 362 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; | 307 const path = '(?:$identOrElementIndex)(?:\\.$identOrElementIndex)*'; |
| 363 return new RegExp('^$path\$'); | 308 return new RegExp('^$path\$'); |
| (...skipping 199 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 563 } | 508 } |
| 564 | 509 |
| 565 _notifyCallback = callback; | 510 _notifyCallback = callback; |
| 566 _notifyArgumentCount = min(_reportArgumentCount, | 511 _notifyArgumentCount = min(_reportArgumentCount, |
| 567 _maxArgumentCount(callback)); | 512 _maxArgumentCount(callback)); |
| 568 | 513 |
| 569 _connect(); | 514 _connect(); |
| 570 return _value; | 515 return _value; |
| 571 } | 516 } |
| 572 | 517 |
| 573 @reflectable get value { | 518 get value { |
| 574 _check(skipChanges: true); | 519 _check(skipChanges: true); |
| 575 return _value; | 520 return _value; |
| 576 } | 521 } |
| 577 | 522 |
| 578 void close() { | 523 void close() { |
| 579 if (!_isOpen) return; | 524 if (!_isOpen) return; |
| 580 | 525 |
| 581 _disconnect(); | 526 _disconnect(); |
| 582 _value = null; | 527 _value = null; |
| 583 _notifyCallback = null; | 528 _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)) { | 674 for (var observer in _observers.values.toList(growable: false)) { |
| 730 if (observer._isOpen) observer._check(); | 675 if (observer._isOpen) observer._check(); |
| 731 } | 676 } |
| 732 | 677 |
| 733 _resetNeeded = true; | 678 _resetNeeded = true; |
| 734 scheduleMicrotask(reset); | 679 scheduleMicrotask(reset); |
| 735 } | 680 } |
| 736 } | 681 } |
| 737 | 682 |
| 738 const int _MAX_DIRTY_CHECK_CYCLES = 1000; | 683 const int _MAX_DIRTY_CHECK_CYCLES = 1000; |
| OLD | NEW |