| 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 |