From 2a455edbc58577768ac8256fe64c7e934c9de2df Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 18 Jul 2018 15:52:43 -0500 Subject: [PATCH 01/21] Clean up some of the animation code. --- src/Avalonia.Animation/Animation.cs | 17 +++++++------- src/Avalonia.Animation/Animator`1.cs | 35 +++++++++++----------------- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index aa436f5f4e..d15a9b78bb 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -82,7 +82,7 @@ namespace Avalonia.Animation private void InterpretKeyframes() { - var handlerList = new List<(Type, AvaloniaProperty)>(); + var handlerList = new List<(Type type, AvaloniaProperty property)>(); var kfList = new List(); foreach (var keyframe in this) @@ -116,18 +116,17 @@ namespace Avalonia.Animation var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>(); - foreach (var handler in handlerList) + foreach (var (handlerType, property) in handlerList) { - var newInstance = (IAnimator)Activator.CreateInstance(handler.Item1); - newInstance.Property = handler.Item2; - newAnimatorInstances.Add((handler.Item1, handler.Item2, newInstance)); + var newInstance = (IAnimator)Activator.CreateInstance(handlerType); + newInstance.Property = property; + newAnimatorInstances.Add((handlerType, property, newInstance)); } foreach (var kf in kfList) { - var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler && - p.prop == kf.Property) - .First(); + var parent = newAnimatorInstances.First(p => p.handler == kf.Handler && + p.prop == kf.Property); parent.inst.Add(kf); } @@ -163,4 +162,4 @@ namespace Avalonia.Animation return this; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 6d4ae7d8e2..2b9f0645b7 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation /// /// List of type-converted keyframes. /// - private Dictionary _convertedKeyframes = new Dictionary(); + private readonly IDictionary _convertedKeyframes = new SortedDictionary(); private bool _isVerfifiedAndConverted; @@ -38,10 +38,9 @@ namespace Avalonia.Animation public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch) { if (!_isVerfifiedAndConverted) - VerifyConvertKeyFrames(animation, typeof(T)); + VerifyConvertKeyFrames(animation); return obsMatch - .Where(p => p == true) // Ignore triggers when global timers are paused. .Where(p => Timing.GetGlobalPlayState() != PlayState.Pause) .Subscribe(_ => @@ -61,7 +60,7 @@ namespace Avalonia.Animation protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t) { KeyValuePair firstCue, lastCue; - int kvCount = _convertedKeyframes.Count(); + int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { if (DoubleUtils.AboutEqual(t, 0.0) || t < 0.0) @@ -76,8 +75,8 @@ namespace Avalonia.Animation } else { - firstCue = _convertedKeyframes.Where(j => j.Key <= t).Last(); - lastCue = _convertedKeyframes.Where(j => j.Key >= t).First(); + firstCue = _convertedKeyframes.Last(j => j.Key <= t); + lastCue = _convertedKeyframes.First(j => j.Key >= t); } } else @@ -103,10 +102,7 @@ namespace Avalonia.Animation Timing.AnimationStateTimer .TakeWhile(_ => !_kfStateMach._unsubscribe) - .Subscribe(p => - { - _kfStateMach.Step(p, DoInterpolation); - }); + .Subscribe(p => _kfStateMach.Step(p, DoInterpolation)); return control.Bind(Property, _kfStateMach, BindingPriority.Animation); } @@ -119,9 +115,9 @@ namespace Avalonia.Animation /// /// Verifies and converts keyframe values according to this class's target type. /// - private void VerifyConvertKeyFrames(Animation animation, Type type) + private void VerifyConvertKeyFrames(Animation animation) { - var typeConv = TypeDescriptor.GetConverter(type); + var typeConv = TypeDescriptor.GetConverter(typeof(T)); foreach (AnimatorKeyFrame k in this) { @@ -129,12 +125,12 @@ namespace Avalonia.Animation { throw new ArgumentNullException($"KeyFrame value can't be null."); } - if (!typeConv.CanConvertTo(k.Value.GetType())) + if (!typeConv.CanConvertFrom(k.Value.GetType())) { throw new InvalidCastException($"KeyFrame value doesnt match property type."); } - T convertedValue = (T)typeConv.ConvertTo(k.Value, type); + T convertedValue = (T)typeConv.ConvertTo(k.Value, typeof(T)); Cue _normalizedCue = k.Cue; @@ -146,12 +142,12 @@ namespace Avalonia.Animation _convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false)); } - SortKeyFrameCues(_convertedKeyframes); + AddNeutralKeyFramesIfNeeded(); _isVerfifiedAndConverted = true; } - private void SortKeyFrameCues(Dictionary convertedValues) + private void AddNeutralKeyFramesIfNeeded() { bool hasStartKey, hasEndKey; hasStartKey = hasEndKey = false; @@ -171,12 +167,9 @@ namespace Avalonia.Animation if (!hasStartKey || !hasEndKey) AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes); - - _convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key) - .ToDictionary((k) => k.Key, (v) => v.Value); } - private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary convertedKeyframes) + private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, IDictionary convertedKeyframes) { if (!hasStartKey) { @@ -189,4 +182,4 @@ namespace Avalonia.Animation } } } -} \ No newline at end of file +} From d4cb79b428b465d95c6c3b963a1d366d4b677ca5 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 18 Jul 2018 18:16:58 -0500 Subject: [PATCH 02/21] Remove goto from AnimatorStateMachine. --- .../AnimatorStateMachine`1.cs | 181 +++++++++--------- 1 file changed, 95 insertions(+), 86 deletions(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index e37b0e592a..a5815956a8 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -123,121 +123,130 @@ namespace Avalonia.Animation double _tempDuration = 0d, _easedTime; - checkstate: - switch (_currentState) + bool handled = false; + + while (!handled) { - case KeyFramesStates.DoDelay: + switch (_currentState) + { + case KeyFramesStates.DoDelay: - if (_fillMode == FillMode.Backward - || _fillMode == FillMode.Both) - { - if (_currentIteration == 0) + if (_fillMode == FillMode.Backward + || _fillMode == FillMode.Both) { - _targetObserver.OnNext(_firstKFValue); + if (_currentIteration == 0) + { + _targetObserver.OnNext(_firstKFValue); + } + else + { + _targetObserver.OnNext(_lastInterpValue); + } + } + + if (_delayFrameCount > _delayTotalFrameCount) + { + _currentState = KeyFramesStates.DoRun; } else { - _targetObserver.OnNext(_lastInterpValue); + handled = true; + _delayFrameCount++; } - } - - if (_delayFrameCount > _delayTotalFrameCount) - { - _currentState = KeyFramesStates.DoRun; - goto checkstate; - } - _delayFrameCount++; - break; - - case KeyFramesStates.DoRun: - - if (_isReversed) - _currentState = KeyFramesStates.RunBackwards; - else - _currentState = KeyFramesStates.RunForwards; - - goto checkstate; - - case KeyFramesStates.RunForwards: - - if (_durationFrameCount > _durationTotalFrameCount) - { - _currentState = KeyFramesStates.RunComplete; - goto checkstate; - } + break; - _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; - _currentState = KeyFramesStates.RunApplyValue; + case KeyFramesStates.DoRun: - goto checkstate; + if (_isReversed) + _currentState = KeyFramesStates.RunBackwards; + else + _currentState = KeyFramesStates.RunForwards; - case KeyFramesStates.RunBackwards: + break; - if (_durationFrameCount > _durationTotalFrameCount) - { - _currentState = KeyFramesStates.RunComplete; - goto checkstate; - } + case KeyFramesStates.RunForwards: - _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; - _currentState = KeyFramesStates.RunApplyValue; + if (_durationFrameCount > _durationTotalFrameCount) + { + _currentState = KeyFramesStates.RunComplete; + } + else + { + _tempDuration = (double)_durationFrameCount / _durationTotalFrameCount; + _currentState = KeyFramesStates.RunApplyValue; - goto checkstate; + } + break; - case KeyFramesStates.RunApplyValue: + case KeyFramesStates.RunBackwards: - _easedTime = _targetAnimation.Easing.Ease(_tempDuration); + if (_durationFrameCount > _durationTotalFrameCount) + { + _currentState = KeyFramesStates.RunComplete; + } + else + { + _tempDuration = (double)(_durationTotalFrameCount - _durationFrameCount) / _durationTotalFrameCount; + _currentState = KeyFramesStates.RunApplyValue; + } + break; - _durationFrameCount++; - _lastInterpValue = Interpolator(_easedTime, _neutralValue); - _targetObserver.OnNext(_lastInterpValue); - _currentState = KeyFramesStates.DoRun; + case KeyFramesStates.RunApplyValue: - break; + _easedTime = _targetAnimation.Easing.Ease(_tempDuration); - case KeyFramesStates.RunComplete: + _durationFrameCount++; + _lastInterpValue = Interpolator(_easedTime, _neutralValue); + _targetObserver.OnNext(_lastInterpValue); + _currentState = KeyFramesStates.DoRun; + handled = true; + break; - if (_checkLoopAndRepeat) - { - _delayFrameCount = 0; - _durationFrameCount = 0; + case KeyFramesStates.RunComplete: - if (_isLooping) - { - _currentState = KeyFramesStates.DoRun; - } - else if (_isRepeating) + if (_checkLoopAndRepeat) { - if (_currentIteration >= _repeatCount) + _delayFrameCount = 0; + _durationFrameCount = 0; + + if (_isLooping) { - _currentState = KeyFramesStates.Stop; + _currentState = KeyFramesStates.DoRun; } - else + else if (_isRepeating) { - _currentState = KeyFramesStates.DoRun; + if (_currentIteration >= _repeatCount) + { + _currentState = KeyFramesStates.Stop; + } + else + { + _currentState = KeyFramesStates.DoRun; + } + _currentIteration++; } - _currentIteration++; - } - if (_animationDirection == PlaybackDirection.Alternate - || _animationDirection == PlaybackDirection.AlternateReverse) - _isReversed = !_isReversed; + if (_animationDirection == PlaybackDirection.Alternate + || _animationDirection == PlaybackDirection.AlternateReverse) + _isReversed = !_isReversed; - break; - } + break; + } - _currentState = KeyFramesStates.Stop; - goto checkstate; + _currentState = KeyFramesStates.Stop; + break; - case KeyFramesStates.Stop: + case KeyFramesStates.Stop: - if (_fillMode == FillMode.Forward - || _fillMode == FillMode.Both) - { - _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); - } - _targetObserver.OnCompleted(); - break; + if (_fillMode == FillMode.Forward + || _fillMode == FillMode.Both) + { + _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); + } + _targetObserver.OnCompleted(); + handled = true; + break; + } } } @@ -253,4 +262,4 @@ namespace Avalonia.Animation _currentState = KeyFramesStates.Disposed; } } -} \ No newline at end of file +} From 4716193a9b81dae66216301ab113aa35f7e89e06 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Wed, 18 Jul 2018 18:35:37 -0500 Subject: [PATCH 03/21] Clean up and fix bug in Animator. --- src/Avalonia.Animation/AnimatorStateMachine`1.cs | 4 ++-- src/Avalonia.Animation/Animator`1.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index a5815956a8..16d081d83d 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -51,9 +51,9 @@ namespace Avalonia.Animation Disposed } - public void Initialize(Animation animation, Animatable control, Animator keyframes) + public void Initialize(Animation animation, Animatable control, Animator animator) { - _parent = keyframes; + _parent = animator; _targetAnimation = animation; _targetControl = control; _neutralValue = (T)_targetControl.GetValue(_parent.Property); diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 2b9f0645b7..7036355ab4 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -125,7 +125,7 @@ namespace Avalonia.Animation { throw new ArgumentNullException($"KeyFrame value can't be null."); } - if (!typeConv.CanConvertFrom(k.Value.GetType())) + if (!typeConv.CanConvertTo(k.Value.GetType())) { throw new InvalidCastException($"KeyFrame value doesnt match property type."); } From c2e2d8b49cd1ddf67877979659dfc3bae8fe3d3a Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 19 Jul 2018 12:57:22 -0500 Subject: [PATCH 04/21] More cleanup. --- src/Avalonia.Animation/Animation.cs | 3 +-- src/Avalonia.Animation/AnimatorKeyFrame.cs | 2 +- src/Avalonia.Animation/AnimatorStateMachine`1.cs | 3 +++ src/Avalonia.Animation/Animator`1.cs | 4 ++-- src/Avalonia.Animation/Cue.cs | 2 +- src/Avalonia.Animation/KeyFrame.cs | 16 +++++++++++----- 6 files changed, 19 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index d15a9b78bb..3b4afa637a 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -105,8 +105,7 @@ namespace Avalonia.Animation Property = setter.Property, Cue = keyframe.Cue, KeyTime = keyframe.KeyTime, - timeSpanSet = keyframe.timeSpanSet, - cueSet = keyframe.cueSet, + TimingMode = keyframe.TimingMode, Value = setter.Value }; diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 02457cb9aa..74061d76db 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -16,7 +16,7 @@ namespace Avalonia.Animation public Type Handler; public Cue Cue; public TimeSpan KeyTime; - internal bool timeSpanSet, cueSet; + internal KeyFrameTimingMode TimingMode; public AvaloniaProperty Property; public object Value; } diff --git a/src/Avalonia.Animation/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index 16d081d83d..1a51b897c0 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -246,6 +246,9 @@ namespace Avalonia.Animation _targetObserver.OnCompleted(); handled = true; break; + default: + handled = true; + break; } } } diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 7036355ab4..dde69cbd65 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation /// /// List of type-converted keyframes. /// - private readonly IDictionary _convertedKeyframes = new SortedDictionary(); + private readonly SortedList _convertedKeyframes = new SortedList(); private bool _isVerfifiedAndConverted; @@ -134,7 +134,7 @@ namespace Avalonia.Animation Cue _normalizedCue = k.Cue; - if (k.timeSpanSet) + if (k.TimingMode == KeyFrameTimingMode.TimeSpan) { _normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks); } diff --git a/src/Avalonia.Animation/Cue.cs b/src/Avalonia.Animation/Cue.cs index fe36b13495..5a95c108e3 100644 --- a/src/Avalonia.Animation/Cue.cs +++ b/src/Avalonia.Animation/Cue.cs @@ -10,7 +10,7 @@ namespace Avalonia.Animation /// A Cue object for . /// [TypeConverter(typeof(CueTypeConverter))] - public struct Cue : IEquatable, IEquatable + public readonly struct Cue : IEquatable, IEquatable { /// /// The normalized percent value, ranging from 0.0 to 1.0 diff --git a/src/Avalonia.Animation/KeyFrame.cs b/src/Avalonia.Animation/KeyFrame.cs index 46be119c36..ea04aa0aab 100644 --- a/src/Avalonia.Animation/KeyFrame.cs +++ b/src/Avalonia.Animation/KeyFrame.cs @@ -7,6 +7,11 @@ using Avalonia.Collections; namespace Avalonia.Animation { + internal enum KeyFrameTimingMode + { + TimeSpan = 1, + Cue + } /// /// Stores data regarding a specific key @@ -14,7 +19,6 @@ namespace Avalonia.Animation /// public class KeyFrame : AvaloniaList { - internal bool timeSpanSet, cueSet; private TimeSpan _ktimeSpan; private Cue _kCue; @@ -30,6 +34,8 @@ namespace Avalonia.Animation { } + internal KeyFrameTimingMode TimingMode { get; private set; } + /// /// Gets or sets the key time of this . /// @@ -42,11 +48,11 @@ namespace Avalonia.Animation } set { - if (cueSet) + if (TimingMode == KeyFrameTimingMode.Cue) { throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); } - timeSpanSet = true; + TimingMode = KeyFrameTimingMode.TimeSpan; _ktimeSpan = value; } } @@ -63,11 +69,11 @@ namespace Avalonia.Animation } set { - if (timeSpanSet) + if (TimingMode == KeyFrameTimingMode.TimeSpan) { throw new InvalidOperationException($"You can only set either {nameof(KeyTime)} or {nameof(Cue)}."); } - cueSet = true; + TimingMode = KeyFrameTimingMode.Cue; _kCue = value; } } From 1a84fe9186d7ffcaa69798e34e4623c5f05d0546 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 19 Jul 2018 16:41:24 -0500 Subject: [PATCH 05/21] Clean up cue handling. --- src/Avalonia.Animation/Animation.cs | 31 +++++++++++++--------- src/Avalonia.Animation/AnimatorKeyFrame.cs | 4 +-- src/Avalonia.Animation/Animator`1.cs | 23 ++++++---------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 3b4afa637a..685f160745 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -83,7 +83,7 @@ namespace Avalonia.Animation private void InterpretKeyframes() { var handlerList = new List<(Type type, AvaloniaProperty property)>(); - var kfList = new List(); + var animatorKeyFrames = new List(); foreach (var keyframe in this) { @@ -99,38 +99,43 @@ namespace Avalonia.Animation if (!handlerList.Contains((handler, setter.Property))) handlerList.Add((handler, setter.Property)); + var cue = keyframe.Cue; + + if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan) + { + cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks); + } + var newKF = new AnimatorKeyFrame() { - Handler = handler, + AnimatorType = handler, Property = setter.Property, - Cue = keyframe.Cue, - KeyTime = keyframe.KeyTime, - TimingMode = keyframe.TimingMode, + Cue = cue, Value = setter.Value }; - kfList.Add(newKF); + animatorKeyFrames.Add(newKF); } } - var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>(); + var newAnimatorInstances = new List(); foreach (var (handlerType, property) in handlerList) { var newInstance = (IAnimator)Activator.CreateInstance(handlerType); newInstance.Property = property; - newAnimatorInstances.Add((handlerType, property, newInstance)); + newAnimatorInstances.Add(newInstance); } - foreach (var kf in kfList) + foreach (var keyframe in animatorKeyFrames) { - var parent = newAnimatorInstances.First(p => p.handler == kf.Handler && - p.prop == kf.Property); - parent.inst.Add(kf); + var animator = newAnimatorInstances.First(a => a.GetType() == keyframe.AnimatorType && + a.Property == keyframe.Property); + animator.Add(keyframe); } foreach(var instance in newAnimatorInstances) - _animators.Add(instance.inst); + _animators.Add(instance); } diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 74061d76db..1b0c38e0fc 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -13,10 +13,8 @@ namespace Avalonia.Animation /// public class AnimatorKeyFrame { - public Type Handler; + public Type AnimatorType; public Cue Cue; - public TimeSpan KeyTime; - internal KeyFrameTimingMode TimingMode; public AvaloniaProperty Property; public object Value; } diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index dde69cbd65..327a2ebd62 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -42,7 +42,7 @@ namespace Avalonia.Animation return obsMatch // Ignore triggers when global timers are paused. - .Where(p => Timing.GetGlobalPlayState() != PlayState.Pause) + .Where(p => p && Timing.GetGlobalPlayState() != PlayState.Pause) .Subscribe(_ => { var timerObs = RunKeyFrames(animation, control); @@ -97,14 +97,14 @@ namespace Avalonia.Animation /// private IDisposable RunKeyFrames(Animation animation, Animatable control) { - var _kfStateMach = new AnimatorStateMachine(); - _kfStateMach.Initialize(animation, control, this); + var stateMachine = new AnimatorStateMachine(); + stateMachine.Initialize(animation, control, this); Timing.AnimationStateTimer - .TakeWhile(_ => !_kfStateMach._unsubscribe) - .Subscribe(p => _kfStateMach.Step(p, DoInterpolation)); + .TakeWhile(_ => !stateMachine._unsubscribe) + .Subscribe(p => stateMachine.Step(p, DoInterpolation)); - return control.Bind(Property, _kfStateMach, BindingPriority.Animation); + return control.Bind(Property, stateMachine, BindingPriority.Animation); } /// @@ -131,15 +131,8 @@ namespace Avalonia.Animation } T convertedValue = (T)typeConv.ConvertTo(k.Value, typeof(T)); - - Cue _normalizedCue = k.Cue; - - if (k.TimingMode == KeyFrameTimingMode.TimeSpan) - { - _normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks); - } - - _convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false)); + + _convertedKeyframes.Add(k.Cue.CueValue, (convertedValue, false)); } AddNeutralKeyFramesIfNeeded(); From 4765596e2236218e282771140244bc9c2aa33042 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 19 Jul 2018 17:33:27 -0500 Subject: [PATCH 06/21] Enable setter bindings for animations. --- src/Avalonia.Animation/Animation.cs | 12 ++-- src/Avalonia.Animation/AnimatorKeyFrame.cs | 64 ++++++++++++++++++++-- src/Avalonia.Animation/Animator`1.cs | 37 +++++-------- src/Avalonia.Animation/DoubleAnimator.cs | 8 +-- src/Avalonia.Animation/IAnimationSetter.cs | 2 +- src/Avalonia.Animation/KeyFramePair`1.cs | 8 +-- 6 files changed, 85 insertions(+), 46 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index 685f160745..dcfeb7f88f 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -68,7 +68,7 @@ namespace Avalonia.Animation /// /// The value fill mode for this animation. /// - public FillMode FillMode { get; set; } + public FillMode FillMode { get; set; } /// /// Easing function to be used. @@ -106,13 +106,9 @@ namespace Avalonia.Animation cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks); } - var newKF = new AnimatorKeyFrame() - { - AnimatorType = handler, - Property = setter.Property, - Cue = cue, - Value = setter.Value - }; + var newKF = new AnimatorKeyFrame(handler, cue); + + _subscription.Add(newKF.BindSetter(setter)); animatorKeyFrames.Add(newKF); } diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 1b0c38e0fc..a975f5028f 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -4,6 +4,8 @@ using System.Text; using System.ComponentModel; using Avalonia.Metadata; using Avalonia.Collections; +using Avalonia.Data; +using Avalonia.Reactive; namespace Avalonia.Animation { @@ -11,11 +13,63 @@ namespace Avalonia.Animation /// Defines a KeyFrame that is used for /// objects. /// - public class AnimatorKeyFrame + public class AnimatorKeyFrame : AvaloniaObject { - public Type AnimatorType; - public Cue Cue; - public AvaloniaProperty Property; - public object Value; + public static readonly DirectProperty ValueProperty = + AvaloniaProperty.RegisterDirect(nameof(Value), k => k._value, (k, v) => k._value = v); + + public AnimatorKeyFrame() + { + + } + + public AnimatorKeyFrame(Type animatorType, Cue cue) + { + AnimatorType = animatorType; + Cue = cue; + } + + public Type AnimatorType { get; } + public Cue Cue { get; } + public AvaloniaProperty Property { get; private set; } + + private object _value; + + public object Value + { + get => _value; + set => SetAndRaise(ValueProperty, ref _value, value); + } + + public IDisposable BindSetter(IAnimationSetter setter) + { + Property = setter.Property; + var value = setter.Value; + + if (value is IBinding binding) + { + return this.Bind(ValueProperty, binding); + } + else + { + return this.Bind(ValueProperty, ObservableEx.SingleValue(value)); + } + } + + public T GetTypedValue() + { + var typeConv = TypeDescriptor.GetConverter(typeof(T)); + + if (Value == null) + { + throw new ArgumentNullException($"KeyFrame value can't be null."); + } + if (!typeConv.CanConvertTo(Value.GetType())) + { + throw new InvalidCastException($"KeyFrame value doesnt match property type."); + } + + return (T)typeConv.ConvertTo(Value, typeof(T)); + } } } diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 327a2ebd62..a1eef87e1e 100644 --- a/src/Avalonia.Animation/Animator`1.cs +++ b/src/Avalonia.Animation/Animator`1.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation /// /// List of type-converted keyframes. /// - private readonly SortedList _convertedKeyframes = new SortedList(); + private readonly SortedList _convertedKeyframes = new SortedList(); private bool _isVerfifiedAndConverted; @@ -38,7 +38,7 @@ namespace Avalonia.Animation public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch) { if (!_isVerfifiedAndConverted) - VerifyConvertKeyFrames(animation); + VerifyConvertKeyFrames(); return obsMatch // Ignore triggers when global timers are paused. @@ -59,7 +59,7 @@ namespace Avalonia.Animation /// The time parameter, relative to the total animation time protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t) { - KeyValuePair firstCue, lastCue; + KeyValuePair firstCue, lastCue; int kvCount = _convertedKeyframes.Count; if (kvCount > 2) { @@ -88,7 +88,9 @@ namespace Avalonia.Animation double t0 = firstCue.Key; double t1 = lastCue.Key; var intraframeTime = (t - t0) / (t1 - t0); - return (intraframeTime, new KeyFramePair(firstCue, lastCue)); + var firstFrameData = (firstCue.Value.frame.GetTypedValue(), firstCue.Value.isNeutral); + var lastFrameData = (lastCue.Value.frame.GetTypedValue(), lastCue.Value.isNeutral); + return (intraframeTime, new KeyFramePair(firstFrameData, lastFrameData)); } @@ -115,24 +117,11 @@ namespace Avalonia.Animation /// /// Verifies and converts keyframe values according to this class's target type. /// - private void VerifyConvertKeyFrames(Animation animation) + private void VerifyConvertKeyFrames() { - var typeConv = TypeDescriptor.GetConverter(typeof(T)); - - foreach (AnimatorKeyFrame k in this) + foreach (AnimatorKeyFrame keyframe in this) { - if (k.Value == null) - { - throw new ArgumentNullException($"KeyFrame value can't be null."); - } - if (!typeConv.CanConvertTo(k.Value.GetType())) - { - throw new InvalidCastException($"KeyFrame value doesnt match property type."); - } - - T convertedValue = (T)typeConv.ConvertTo(k.Value, typeof(T)); - - _convertedKeyframes.Add(k.Cue.CueValue, (convertedValue, false)); + _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false)); } AddNeutralKeyFramesIfNeeded(); @@ -159,19 +148,19 @@ namespace Avalonia.Animation } if (!hasStartKey || !hasEndKey) - AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes); + AddNeutralKeyFrames(hasStartKey, hasEndKey); } - private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, IDictionary convertedKeyframes) + private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey) { if (!hasStartKey) { - convertedKeyframes.Add(0.0d, (default(T), true)); + _convertedKeyframes.Add(0.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); } if (!hasEndKey) { - convertedKeyframes.Add(1.0d, (default(T), true)); + _convertedKeyframes.Add(1.0d, (new AnimatorKeyFrame { Value = default(T) }, true)); } } } diff --git a/src/Avalonia.Animation/DoubleAnimator.cs b/src/Avalonia.Animation/DoubleAnimator.cs index 5b994377f1..154f37360c 100644 --- a/src/Avalonia.Animation/DoubleAnimator.cs +++ b/src/Avalonia.Animation/DoubleAnimator.cs @@ -24,15 +24,15 @@ namespace Avalonia.Animation var firstKF = pair.KFPair.FirstKeyFrame; var secondKF = pair.KFPair.SecondKeyFrame; - if (firstKF.Value.isNeutral) + if (firstKF.isNeutral) y0 = neutralValue; else - y0 = firstKF.Value.TargetValue; + y0 = firstKF.TargetValue; - if (secondKF.Value.isNeutral) + if (secondKF.isNeutral) y1 = neutralValue; else - y1 = secondKF.Value.TargetValue; + y1 = secondKF.TargetValue; // Do linear parametric interpolation return y0 + (pair.IntraKFTime) * (y1 - y0); diff --git a/src/Avalonia.Animation/IAnimationSetter.cs b/src/Avalonia.Animation/IAnimationSetter.cs index f2a94c9ed6..2d22377286 100644 --- a/src/Avalonia.Animation/IAnimationSetter.cs +++ b/src/Avalonia.Animation/IAnimationSetter.cs @@ -5,4 +5,4 @@ namespace Avalonia.Animation AvaloniaProperty Property { get; set; } object Value { get; set; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/KeyFramePair`1.cs b/src/Avalonia.Animation/KeyFramePair`1.cs index c192479a1d..408b13e0d8 100644 --- a/src/Avalonia.Animation/KeyFramePair`1.cs +++ b/src/Avalonia.Animation/KeyFramePair`1.cs @@ -22,7 +22,7 @@ namespace Avalonia.Animation /// /// /// - public KeyFramePair(KeyValuePair FirstKeyFrame, KeyValuePair LastKeyFrame) : this() + public KeyFramePair((T TargetValue, bool isNeutral) FirstKeyFrame, (T TargetValue, bool isNeutral) LastKeyFrame) : this() { this.FirstKeyFrame = FirstKeyFrame; this.SecondKeyFrame = LastKeyFrame; @@ -31,11 +31,11 @@ namespace Avalonia.Animation /// /// First object. /// - public KeyValuePair FirstKeyFrame { get; private set; } + public (T TargetValue, bool isNeutral) FirstKeyFrame { get; } /// /// Second object. /// - public KeyValuePair SecondKeyFrame { get; private set; } + public (T TargetValue, bool isNeutral) SecondKeyFrame { get; } } -} \ No newline at end of file +} From a52d9c812d3968aa0bdf7e33186ef0f64fe444fc Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 19 Jul 2018 22:47:32 -0500 Subject: [PATCH 07/21] Use the target Animatible as an anchor for the binding. --- src/Avalonia.Animation/Animation.cs | 18 +++++------------- src/Avalonia.Animation/AnimatorKeyFrame.cs | 6 +++--- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/Avalonia.Animation/Animation.cs b/src/Avalonia.Animation/Animation.cs index dcfeb7f88f..4e777b36ed 100644 --- a/src/Avalonia.Animation/Animation.cs +++ b/src/Avalonia.Animation/Animation.cs @@ -80,7 +80,7 @@ namespace Avalonia.Animation this.CollectionChanged += delegate { _isChildrenChanged = true; }; } - private void InterpretKeyframes() + private IList InterpretKeyframes(Animatable control) { var handlerList = new List<(Type type, AvaloniaProperty property)>(); var animatorKeyFrames = new List(); @@ -108,7 +108,7 @@ namespace Avalonia.Animation var newKF = new AnimatorKeyFrame(handler, cue); - _subscription.Add(newKF.BindSetter(setter)); + _subscription.Add(newKF.BindSetter(setter, control)); animatorKeyFrames.Add(newKF); } @@ -130,9 +130,7 @@ namespace Avalonia.Animation animator.Add(keyframe); } - foreach(var instance in newAnimatorInstances) - _animators.Add(instance); - + return newAnimatorInstances; } /// @@ -149,15 +147,9 @@ namespace Avalonia.Animation /// public IDisposable Apply(Animatable control, IObservable matchObs) { - if (_isChildrenChanged) - { - InterpretKeyframes(); - _isChildrenChanged = false; - } - - foreach (IAnimator keyframes in _animators) + foreach (IAnimator animator in InterpretKeyframes(control)) { - _subscription.Add(keyframes.Apply(this, control, matchObs)); + _subscription.Add(animator.Apply(this, control, matchObs)); } return this; } diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index a975f5028f..bd9c7a0184 100644 --- a/src/Avalonia.Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Animation/AnimatorKeyFrame.cs @@ -41,18 +41,18 @@ namespace Avalonia.Animation set => SetAndRaise(ValueProperty, ref _value, value); } - public IDisposable BindSetter(IAnimationSetter setter) + public IDisposable BindSetter(IAnimationSetter setter, Animatable targetControl) { Property = setter.Property; var value = setter.Value; if (value is IBinding binding) { - return this.Bind(ValueProperty, binding); + return this.Bind(ValueProperty, binding, targetControl); } else { - return this.Bind(ValueProperty, ObservableEx.SingleValue(value)); + return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl); } } From 1afd19efacd8e539ee8a8a9fa0b1e951cbd6a494 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 19 Jul 2018 22:48:01 -0500 Subject: [PATCH 08/21] Use the anchor for RelativeSourceMode.TemplatedParent bindings if needed. --- src/Markup/Avalonia.Markup/Data/Binding.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index e5f7ea1742..867999dc91 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -132,7 +132,10 @@ namespace Avalonia.Data } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentObserver(target, Path, enableDataValidation); + observer = CreateTemplatedParentObserver( + (target as IStyledElement) ?? (anchor as IStyledElement), + Path, + enableDataValidation); } else if (RelativeSource.Mode == RelativeSourceMode.FindAncestor) { From 238974ec48021dd3c1986f2315c62576246a87a8 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 19 Jul 2018 22:51:19 -0500 Subject: [PATCH 09/21] Fix binding value in Setter throwing on default binding mode. --- src/Avalonia.Styling/Styling/Setter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 1a78e0f4d7..6510e639f4 100644 --- a/src/Avalonia.Styling/Styling/Setter.cs +++ b/src/Avalonia.Styling/Styling/Setter.cs @@ -126,7 +126,7 @@ namespace Avalonia.Styling if (source != null) { - var cloned = Clone(source, style, activator); + var cloned = Clone(source, source.Mode == BindingMode.Default ? Property.GetMetadata(control.GetType()).DefaultBindingMode : source.Mode, style, activator); return BindingOperations.Apply(control, Property, cloned, null); } } @@ -134,13 +134,13 @@ namespace Avalonia.Styling return Disposable.Empty; } - private InstancedBinding Clone(InstancedBinding sourceInstance, IStyle style, IObservable activator) + private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable activator) { if (activator != null) { var description = style?.ToString(); - switch (sourceInstance.Mode) + switch (mode) { case BindingMode.OneTime: if (sourceInstance.Observable != null) From b1cb87b770a9396cb239770657981e164fe72a97 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 19 Jul 2018 22:53:06 -0500 Subject: [PATCH 10/21] Implement ProgressBar animation in XAML. Contributes to #1670. --- src/Avalonia.Controls/ProgressBar.cs | 71 ++++++-------------- src/Avalonia.Themes.Default/ProgressBar.xaml | 41 +++++++++-- 2 files changed, 54 insertions(+), 58 deletions(-) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 5e5a460368..af152cf247 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -21,18 +21,18 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + private static readonly StyledProperty IndeterminateStartingOffsetProperty = + AvaloniaProperty.Register(nameof(IndeterminateStartingOffset)); + private Border _indicator; - private IndeterminateAnimation _indeterminateAnimation; static ProgressBar() { PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + PseudoClass(IsIndeterminateProperty, ":indeterminate"); ValueProperty.Changed.AddClassHandler(x => x.ValueChanged); - - IsIndeterminateProperty.Changed.AddClassHandler( - (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); }); } public bool IsIndeterminate @@ -47,6 +47,12 @@ namespace Avalonia.Controls set => SetValue(OrientationProperty, value); } + private double IndeterminateStartingOffset + { + get => GetValue(IndeterminateStartingOffsetProperty); + set => SetValue(IndeterminateStartingOffsetProperty, value); + } + /// protected override Size ArrangeOverride(Size finalSize) { @@ -60,7 +66,6 @@ namespace Avalonia.Controls _indicator = e.NameScope.Get("PART_Indicator"); UpdateIndicator(Bounds.Size); - UpdateIsIndeterminate(IsIndeterminate); } private void UpdateIndicator(Size bounds) @@ -70,9 +75,17 @@ namespace Avalonia.Controls if (IsIndeterminate) { if (Orientation == Orientation.Horizontal) - _indicator.Width = bounds.Width / 5.0; + { + var width = bounds.Width / 5.0; + IndeterminateStartingOffset = -width; + _indicator.Width = width; + } else - _indicator.Height = bounds.Height / 5.0; + { + var height = bounds.Height / 5.0; + IndeterminateStartingOffset = -height; + _indicator.Height = height; + } } else { @@ -86,53 +99,9 @@ namespace Avalonia.Controls } } - private void UpdateIsIndeterminate(bool isIndeterminate) - { - if (isIndeterminate) - { - if (_indeterminateAnimation == null || _indeterminateAnimation.Disposed) - _indeterminateAnimation = IndeterminateAnimation.StartAnimation(this); - } - else - _indeterminateAnimation?.Dispose(); - } - private void ValueChanged(AvaloniaPropertyChangedEventArgs e) { UpdateIndicator(Bounds.Size); } - - // TODO: Implement Indeterminate Progress animation - // in xaml (most ideal) or if it's not possible - // then on this class. - private class IndeterminateAnimation : IDisposable - { - private WeakReference _progressBar; - - private bool _disposed; - - public bool Disposed => _disposed; - - private IndeterminateAnimation(ProgressBar progressBar) - { - _progressBar = new WeakReference(progressBar); - - } - - public static IndeterminateAnimation StartAnimation(ProgressBar progressBar) - { - return new IndeterminateAnimation(progressBar); - } - - private Rect GetAnimationRect(TimeSpan time) - { - return Rect.Empty; - } - - public void Dispose() - { - _disposed = true; - } - } } } diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index c9c898562c..df735e2048 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -7,14 +7,11 @@ - - + - + Background="{TemplateBinding Foreground}"/> + @@ -22,10 +19,12 @@ - \ No newline at end of file + + + From dcafd6e764294dba98435345730ed479691cbd16 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Thu, 19 Jul 2018 23:20:18 -0500 Subject: [PATCH 11/21] Use transforms instead of Canvas to support the ProgressBar indeterminate animation. --- src/Avalonia.Controls/ProgressBar.cs | 14 ++++++++++++- src/Avalonia.Themes.Default/ProgressBar.xaml | 22 ++++++++++---------- 2 files changed, 24 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index af152cf247..a42d793ff1 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -24,6 +24,9 @@ namespace Avalonia.Controls private static readonly StyledProperty IndeterminateStartingOffsetProperty = AvaloniaProperty.Register(nameof(IndeterminateStartingOffset)); + private static readonly StyledProperty IndeterminateEndingOffsetProperty = + AvaloniaProperty.Register(nameof(IndeterminateEndingOffset)); + private Border _indicator; static ProgressBar() @@ -53,6 +56,12 @@ namespace Avalonia.Controls set => SetValue(IndeterminateStartingOffsetProperty, value); } + private double IndeterminateEndingOffset + { + get => GetValue(IndeterminateEndingOffsetProperty); + set => SetValue(IndeterminateEndingOffsetProperty, value); + } + /// protected override Size ArrangeOverride(Size finalSize) { @@ -79,12 +88,15 @@ namespace Avalonia.Controls var width = bounds.Width / 5.0; IndeterminateStartingOffset = -width; _indicator.Width = width; + IndeterminateEndingOffset = bounds.Width; + } else { var height = bounds.Height / 5.0; - IndeterminateStartingOffset = -height; + IndeterminateStartingOffset = -bounds.Height; _indicator.Height = height; + IndeterminateEndingOffset = height; } } else diff --git a/src/Avalonia.Themes.Default/ProgressBar.xaml b/src/Avalonia.Themes.Default/ProgressBar.xaml index df735e2048..c4cbfed350 100644 --- a/src/Avalonia.Themes.Default/ProgressBar.xaml +++ b/src/Avalonia.Themes.Default/ProgressBar.xaml @@ -7,11 +7,9 @@ - - - + @@ -19,12 +17,10 @@ + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = (TextBlock)window.Content; + + window.DataContext = 5.6; + window.ApplyTemplate(); + + Assert.Equal(5.6, AttachedPropertyOwner.GetDouble(textBlock)); + } + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 2c7e850fee..4517aa6aa1 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -146,5 +146,31 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml Assert.NotNull(target.FocusAdorner); } } + + [Fact] + public void Setter_Can_Set_Attached_Property() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = (TextBlock)window.Content; + + window.ApplyTemplate(); + + Assert.Equal(Dock.Right, DockPanel.GetDock(textBlock)); + } + } } -} \ No newline at end of file +} From ffcaa545bb64db5489de3cb135e4f831afb07a8f Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 10:45:07 +0200 Subject: [PATCH 16/21] Added PropertyParser. So we don't need to use a regex to parse property strings. --- .../Avalonia.Markup.Xaml.csproj | 1 + .../Parsers/PropertyParser.cs | 84 +++++++ .../Markup/Parsers/IdentifierParser.cs | 2 +- .../Avalonia.Markup/Markup/Parsers/Reader.cs | 2 +- .../Parsers/PropertyParserTests.cs | 225 ++++++++++++++++++ 5 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs create mode 100644 tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index cdc22f4102..8c843a4b49 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs new file mode 100644 index 0000000000..ce82ffe0a1 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -0,0 +1,84 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; + +namespace Avalonia.Markup.Xaml.Parsers +{ + internal class PropertyParser + { + public (string ns, string owner, string name) Parse(Reader r) + { + if (r.End) + { + throw new ExpressionParseException(0, "Expected property name."); + } + + var openParens = r.TakeIf('('); + bool closeParens = false; + string ns = null; + string owner = null; + string name = null; + + do + { + var token = IdentifierParser.Parse(r); + + if (token == null) + { + if (r.End) + { + break; + } + else + { + if (openParens && !r.End && (closeParens = r.TakeIf(')'))) + { + break; + } + else if (openParens) + { + throw new ExpressionParseException(r.Position, $"Expected ')'."); + } + + throw new ExpressionParseException(r.Position, $"Unexpected '{r.Peek}'."); + } + } + else if (!r.End && r.TakeIf(':')) + { + ns = ns == null ? + token : + throw new ExpressionParseException(r.Position, "Unexpected ':'."); + } + else if (!r.End && r.TakeIf('.')) + { + owner = owner == null ? + token : + throw new ExpressionParseException(r.Position, "Unexpected '.'."); + } + else + { + name = token; + } + } while (!r.End); + + if (name == null) + { + throw new ExpressionParseException(0, "Expected property name."); + } + else if (openParens && owner == null) + { + throw new ExpressionParseException(1, "Expected property owner."); + } + else if (openParens && !closeParens) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } + else if (!r.End) + { + throw new ExpressionParseException(r.Position, "Expected end of expression."); + } + + return (ns, owner, name); + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs index f86f2db321..9431dab45e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs @@ -6,7 +6,7 @@ using System.Text; namespace Avalonia.Markup.Parsers { - internal static class IdentifierParser + public static class IdentifierParser { public static string Parse(Reader r) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs index 9355bc9aa3..4a3d6aa277 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs @@ -5,7 +5,7 @@ using System; namespace Avalonia.Markup.Parsers { - internal class Reader + public class Reader { private readonly string _s; private int _i; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs new file mode 100644 index 0000000000..a05485f55b --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs @@ -0,0 +1,225 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.Parsers; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.Parsers +{ + public class PropertyParserTests + { + [Fact] + public void Parses_Name() + { + var target = new PropertyParser(); + var reader = new Reader("Foo"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Null(ns); + Assert.Null(owner); + Assert.Equal("Foo", name); + } + + [Fact] + public void Parses_Owner_And_Name() + { + var target = new PropertyParser(); + var reader = new Reader("Foo.Bar"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Null(ns); + Assert.Equal("Foo", owner); + Assert.Equal("Bar", name); + } + + [Fact] + public void Parses_Namespace_Owner_And_Name() + { + var target = new PropertyParser(); + var reader = new Reader("foo:Bar.Baz"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Equal("foo", ns); + Assert.Equal("Bar", owner); + Assert.Equal("Baz", name); + } + + [Fact] + public void Parses_Owner_And_Name_With_Parentheses() + { + var target = new PropertyParser(); + var reader = new Reader("(Foo.Bar)"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Null(ns); + Assert.Equal("Foo", owner); + Assert.Equal("Bar", name); + } + + [Fact] + public void Parses_Namespace_Owner_And_Name_With_Parentheses() + { + var target = new PropertyParser(); + var reader = new Reader("(foo:Bar.Baz)"); + var (ns, owner, name) = target.Parse(reader); + + Assert.Equal("foo", ns); + Assert.Equal("Bar", owner); + Assert.Equal("Baz", name); + } + + [Fact] + public void Fails_With_Empty_String() + { + var target = new PropertyParser(); + var reader = new Reader(""); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(0, ex.Column); + Assert.Equal("Expected property name.", ex.Message); + } + + [Fact] + public void Fails_With_Only_Whitespace() + { + var target = new PropertyParser(); + var reader = new Reader(" "); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(0, ex.Column); + Assert.Equal("Unexpected ' '.", ex.Message); + } + + [Fact] + public void Fails_With_Leading_Whitespace() + { + var target = new PropertyParser(); + var reader = new Reader(" Foo"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(0, ex.Column); + Assert.Equal("Unexpected ' '.", ex.Message); + } + + [Fact] + public void Fails_With_Trailing_Whitespace() + { + var target = new PropertyParser(); + var reader = new Reader("Foo "); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(3, ex.Column); + Assert.Equal("Unexpected ' '.", ex.Message); + } + + [Fact] + public void Fails_With_Invalid_Property_Name() + { + var target = new PropertyParser(); + var reader = new Reader("123"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(0, ex.Column); + Assert.Equal("Unexpected '1'.", ex.Message); + } + + [Fact] + public void Fails_With_Trailing_Junk() + { + var target = new PropertyParser(); + var reader = new Reader("Foo%"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(3, ex.Column); + Assert.Equal("Unexpected '%'.", ex.Message); + } + + [Fact] + public void Fails_With_Invalid_Property_Name_After_Owner() + { + var target = new PropertyParser(); + var reader = new Reader("Foo.123"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(4, ex.Column); + Assert.Equal("Unexpected '1'.", ex.Message); + } + + [Fact] + public void Fails_With_Whitespace_Between_Owner_And_Name() + { + var target = new PropertyParser(); + var reader = new Reader("Foo. Bar"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(4, ex.Column); + Assert.Equal("Unexpected ' '.", ex.Message); + } + + [Fact] + public void Fails_With_Too_Many_Segments() + { + var target = new PropertyParser(); + var reader = new Reader("Foo.Bar.Baz"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(8, ex.Column); + Assert.Equal("Unexpected '.'.", ex.Message); + } + + [Fact] + public void Fails_With_Too_Many_Namespaces() + { + var target = new PropertyParser(); + var reader = new Reader("foo:bar:Baz"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(8, ex.Column); + Assert.Equal("Unexpected ':'.", ex.Message); + } + + [Fact] + public void Fails_With_Parens_But_No_Owner() + { + var target = new PropertyParser(); + var reader = new Reader("(Foo)"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(1, ex.Column); + Assert.Equal("Expected property owner.", ex.Message); + } + + [Fact] + public void Fails_With_Parens_And_Namespace_But_No_Owner() + { + var target = new PropertyParser(); + var reader = new Reader("(foo:Bar)"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(1, ex.Column); + Assert.Equal("Expected property owner.", ex.Message); + } + + [Fact] + public void Fails_With_Missing_Close_Parens() + { + var target = new PropertyParser(); + var reader = new Reader("(Foo.Bar"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(8, ex.Column); + Assert.Equal("Expected ')'.", ex.Message); + } + + [Fact] + public void Fails_With_Unexpected_Close_Parens() + { + var target = new PropertyParser(); + var reader = new Reader("Foo.Bar)"); + + var ex = Assert.Throws(() => target.Parse(reader)); + Assert.Equal(7, ex.Column); + Assert.Equal("Unexpected ')'.", ex.Message); + } + } +} From 0b796adc53f923208b41991b6806141492dac8a2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 10:57:43 +0200 Subject: [PATCH 17/21] Fix a few issues with AvaloniaPropertyTypeConverter - Don't use regex to parse property strings, use `PropertyParser` - Handle XAML namespaces on attached properties Fixes #1764 --- .../AvaloniaPropertyTypeConverter.cs | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 63b7811dbc..6cdf0452d0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -4,19 +4,16 @@ using System; using System.ComponentModel; using System.Globalization; -using System.Text.RegularExpressions; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; -using Portable.Xaml; using Portable.Xaml.ComponentModel; -using Portable.Xaml.Markup; namespace Avalonia.Markup.Xaml.Converters { public class AvaloniaPropertyTypeConverter : TypeConverter { - private static readonly Regex regex = new Regex(@"^\(?(\w*)\.(\w*)\)?|(.*)$"); - public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) { return sourceType == typeof(string); @@ -24,8 +21,10 @@ namespace Avalonia.Markup.Xaml.Converters public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) { - var (owner, propertyName) = ParseProperty((string)value); - var ownerType = TryResolveOwnerByName(context, owner) ?? + var parser = new PropertyParser(); + var reader = new Reader((string)value); + var (ns, owner, propertyName) = parser.Parse(reader); + var ownerType = TryResolveOwnerByName(context, ns, owner) ?? context.GetFirstAmbientValue()?.TargetType ?? context.GetFirstAmbientValue + + +"; + var loader = new AvaloniaXamlLoader(); + var ex = Assert.Throws(() => loader.Load(xaml)); + + Assert.Equal( + "Property 'Button.IsDefault' is not registered on 'Avalonia.Controls.TextBlock'.", + ex.InnerException.Message); + } + } } } From e0be7353a88e37957173a2fd19df643b206eb4c2 Mon Sep 17 00:00:00 2001 From: Jeremy Koritzinsky Date: Fri, 20 Jul 2018 14:02:43 -0500 Subject: [PATCH 19/21] Fix tests that broke from the API change. --- .../Parsers/ExpressionNodeBuilderTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs index 212b16965c..2d687ff4f7 100644 --- a/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs +++ b/tests/Avalonia.Markup.UnitTests/Parsers/ExpressionNodeBuilderTests.cs @@ -162,8 +162,9 @@ namespace Avalonia.Markup.UnitTests.Parsers Assert.Equal(e.Arguments.ToArray(), args); } - private List ToList(ExpressionNode node) + private List ToList((ExpressionNode node, SourceMode mode) parsed) { + var (node, _) = parsed; var result = new List(); while (node != null) From 62526bef3599681e08d9b6a2ed51d8b9316f0165 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 21:38:14 +0200 Subject: [PATCH 20/21] Don't disallow setting unregistered properties. The changes made to the animation system in #1768 currently needs to be able to set any property on any object in order for animations on transforms to work. --- .../Converters/AvaloniaPropertyTypeConverter.cs | 9 ++++++++- tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs | 2 +- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 2470778685..0588e82901 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -5,6 +5,7 @@ using System; using System.ComponentModel; using System.Globalization; using Avalonia.Controls; +using Avalonia.Logging; using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; @@ -42,7 +43,13 @@ namespace Avalonia.Markup.Xaml.Converters !property.IsAttached && !registry.IsRegistered(targetType, property)) { - throw new XamlLoadException($"Property '{effectiveOwner.Name}.{propertyName}' is not registered on '{targetType}'."); + Logger.Warning( + LogArea.Property, + this, + "Property '{Owner}.{Name}' is not registered on '{Type}'.", + effectiveOwner, + propertyName, + targetType); } return property; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs index 1d2f90d383..beaf7477d0 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs @@ -174,7 +174,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml } } - [Fact] + [Fact(Skip = "The animation system currently needs to be able to set any property on any object")] public void Disallows_Setting_Non_Registered_Property() { using (UnitTestApplication.Start(TestServices.StyledWindow)) From dc6f14e77550bbf898d48a93f92ac4f1b1fafb8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 20 Jul 2018 23:04:28 +0200 Subject: [PATCH 21/21] Reader -> CharacterReader And moved `CharacterReader` and `IdentifierParser` into Avalonia.Utilities. --- .../Utilities/CharacterReader.cs} | 6 +-- .../Utilities}/IdentifierParser.cs | 4 +- .../AvaloniaPropertyTypeConverter.cs | 3 +- .../Parsers/PropertyParser.cs | 3 +- .../Markup/Parsers/ArgumentListParser.cs | 3 +- .../Parsers/ExpressionObserverBuilder.cs | 3 +- .../Markup/Parsers/ExpressionParser.cs | 23 +++++------ .../Parsers/PropertyParserTests.cs | 39 ++++++++++--------- 8 files changed, 45 insertions(+), 39 deletions(-) rename src/{Markup/Avalonia.Markup/Markup/Parsers/Reader.cs => Avalonia.Base/Utilities/CharacterReader.cs} (89%) rename src/{Markup/Avalonia.Markup/Markup/Parsers => Avalonia.Base/Utilities}/IdentifierParser.cs (94%) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs similarity index 89% rename from src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs rename to src/Avalonia.Base/Utilities/CharacterReader.cs index 4a3d6aa277..0910d5b969 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -3,14 +3,14 @@ using System; -namespace Avalonia.Markup.Parsers +namespace Avalonia.Utilities { - public class Reader + public class CharacterReader { private readonly string _s; private int _i; - public Reader(string s) + public CharacterReader(string s) { _s = s; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs similarity index 94% rename from src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs rename to src/Avalonia.Base/Utilities/IdentifierParser.cs index 9431dab45e..14b8affbdd 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Text; -namespace Avalonia.Markup.Parsers +namespace Avalonia.Utilities { public static class IdentifierParser { - public static string Parse(Reader r) + public static string Parse(CharacterReader r) { if (IsValidIdentifierStart(r.Peek)) { diff --git a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 0588e82901..627a646bcf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -10,6 +10,7 @@ using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; +using Avalonia.Utilities; using Portable.Xaml.ComponentModel; namespace Avalonia.Markup.Xaml.Converters @@ -25,7 +26,7 @@ namespace Avalonia.Markup.Xaml.Converters { var registry = AvaloniaPropertyRegistry.Instance; var parser = new PropertyParser(); - var reader = new Reader((string)value); + var reader = new CharacterReader((string)value); var (ns, owner, propertyName) = parser.Parse(reader); var ownerType = TryResolveOwnerByName(context, ns, owner); var targetType = context.GetFirstAmbientValue()?.TargetType ?? diff --git a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs index ce82ffe0a1..702758efae 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Parsers/PropertyParser.cs @@ -1,12 +1,13 @@ using System; using Avalonia.Data.Core; using Avalonia.Markup.Parsers; +using Avalonia.Utilities; namespace Avalonia.Markup.Xaml.Parsers { internal class PropertyParser { - public (string ns, string owner, string name) Parse(Reader r) + public (string ns, string owner, string name) Parse(CharacterReader r) { if (r.End) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index ae48657c01..89ef5dcabc 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using Avalonia.Data.Core; +using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Text; @@ -10,7 +11,7 @@ namespace Avalonia.Markup.Parsers { internal static class ArgumentListParser { - public static IList Parse(Reader r, char open, char close) + public static IList Parse(CharacterReader r, char open, char close) { if (r.Peek == open) { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs index ddbe252fc0..6cfb86634b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionObserverBuilder.cs @@ -1,4 +1,5 @@ using Avalonia.Data.Core; +using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Reactive; @@ -15,7 +16,7 @@ namespace Avalonia.Markup.Parsers return new EmptyExpressionNode(); } - var reader = new Reader(expression); + var reader = new CharacterReader(expression); var parser = new ExpressionParser(enableValidation, typeResolver); var node = parser.Parse(reader); diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index 95bb421777..682be572da 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -3,6 +3,7 @@ using Avalonia.Data.Core; using Avalonia.Markup.Parsers.Nodes; +using Avalonia.Utilities; using System; using System.Collections.Generic; using System.Linq; @@ -20,7 +21,7 @@ namespace Avalonia.Markup.Parsers _enableValidation = enableValidation; } - public ExpressionNode Parse(Reader r) + public ExpressionNode Parse(CharacterReader r) { var nodes = new List(); var state = State.Start; @@ -64,7 +65,7 @@ namespace Avalonia.Markup.Parsers return nodes.FirstOrDefault(); } - private State ParseStart(Reader r, IList nodes) + private State ParseStart(CharacterReader r, IList nodes) { if (ParseNot(r)) { @@ -93,7 +94,7 @@ namespace Avalonia.Markup.Parsers return State.End; } - private static State ParseAfterMember(Reader r, IList nodes) + private static State ParseAfterMember(CharacterReader r, IList nodes) { if (ParseMemberAccessor(r)) { @@ -112,7 +113,7 @@ namespace Avalonia.Markup.Parsers return State.End; } - private State ParseBeforeMember(Reader r, IList nodes) + private State ParseBeforeMember(CharacterReader r, IList nodes) { if (ParseOpenBrace(r)) { @@ -132,7 +133,7 @@ namespace Avalonia.Markup.Parsers } } - private State ParseAttachedProperty(Reader r, List nodes) + private State ParseAttachedProperty(CharacterReader r, List nodes) { string ns = string.Empty; string owner; @@ -171,7 +172,7 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private State ParseIndexer(Reader r, List nodes) + private State ParseIndexer(CharacterReader r, List nodes) { var args = ArgumentListParser.Parse(r, '[', ']'); @@ -184,27 +185,27 @@ namespace Avalonia.Markup.Parsers return State.AfterMember; } - private static bool ParseNot(Reader r) + private static bool ParseNot(CharacterReader r) { return !r.End && r.TakeIf('!'); } - private static bool ParseMemberAccessor(Reader r) + private static bool ParseMemberAccessor(CharacterReader r) { return !r.End && r.TakeIf('.'); } - private static bool ParseOpenBrace(Reader r) + private static bool ParseOpenBrace(CharacterReader r) { return !r.End && r.TakeIf('('); } - private static bool PeekOpenBracket(Reader r) + private static bool PeekOpenBracket(CharacterReader r) { return !r.End && r.Peek == '['; } - private static bool ParseStreamOperator(Reader r) + private static bool ParseStreamOperator(CharacterReader r) { return !r.End && r.TakeIf('^'); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs index a05485f55b..cae6449722 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Parsers/PropertyParserTests.cs @@ -2,6 +2,7 @@ using Avalonia.Data.Core; using Avalonia.Markup.Parsers; using Avalonia.Markup.Xaml.Parsers; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Markup.Xaml.UnitTests.Parsers @@ -12,7 +13,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Name() { var target = new PropertyParser(); - var reader = new Reader("Foo"); + var reader = new CharacterReader("Foo"); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -24,7 +25,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Owner_And_Name() { var target = new PropertyParser(); - var reader = new Reader("Foo.Bar"); + var reader = new CharacterReader("Foo.Bar"); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -36,7 +37,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Namespace_Owner_And_Name() { var target = new PropertyParser(); - var reader = new Reader("foo:Bar.Baz"); + var reader = new CharacterReader("foo:Bar.Baz"); var (ns, owner, name) = target.Parse(reader); Assert.Equal("foo", ns); @@ -48,7 +49,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Owner_And_Name_With_Parentheses() { var target = new PropertyParser(); - var reader = new Reader("(Foo.Bar)"); + var reader = new CharacterReader("(Foo.Bar)"); var (ns, owner, name) = target.Parse(reader); Assert.Null(ns); @@ -60,7 +61,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Parses_Namespace_Owner_And_Name_With_Parentheses() { var target = new PropertyParser(); - var reader = new Reader("(foo:Bar.Baz)"); + var reader = new CharacterReader("(foo:Bar.Baz)"); var (ns, owner, name) = target.Parse(reader); Assert.Equal("foo", ns); @@ -72,7 +73,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Empty_String() { var target = new PropertyParser(); - var reader = new Reader(""); + var reader = new CharacterReader(""); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -83,7 +84,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Only_Whitespace() { var target = new PropertyParser(); - var reader = new Reader(" "); + var reader = new CharacterReader(" "); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -94,7 +95,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Leading_Whitespace() { var target = new PropertyParser(); - var reader = new Reader(" Foo"); + var reader = new CharacterReader(" Foo"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -105,7 +106,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Trailing_Whitespace() { var target = new PropertyParser(); - var reader = new Reader("Foo "); + var reader = new CharacterReader("Foo "); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(3, ex.Column); @@ -116,7 +117,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Invalid_Property_Name() { var target = new PropertyParser(); - var reader = new Reader("123"); + var reader = new CharacterReader("123"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(0, ex.Column); @@ -127,7 +128,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Trailing_Junk() { var target = new PropertyParser(); - var reader = new Reader("Foo%"); + var reader = new CharacterReader("Foo%"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(3, ex.Column); @@ -138,7 +139,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Invalid_Property_Name_After_Owner() { var target = new PropertyParser(); - var reader = new Reader("Foo.123"); + var reader = new CharacterReader("Foo.123"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(4, ex.Column); @@ -149,7 +150,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Whitespace_Between_Owner_And_Name() { var target = new PropertyParser(); - var reader = new Reader("Foo. Bar"); + var reader = new CharacterReader("Foo. Bar"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(4, ex.Column); @@ -160,7 +161,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Too_Many_Segments() { var target = new PropertyParser(); - var reader = new Reader("Foo.Bar.Baz"); + var reader = new CharacterReader("Foo.Bar.Baz"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(8, ex.Column); @@ -171,7 +172,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Too_Many_Namespaces() { var target = new PropertyParser(); - var reader = new Reader("foo:bar:Baz"); + var reader = new CharacterReader("foo:bar:Baz"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(8, ex.Column); @@ -182,7 +183,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Parens_But_No_Owner() { var target = new PropertyParser(); - var reader = new Reader("(Foo)"); + var reader = new CharacterReader("(Foo)"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(1, ex.Column); @@ -193,7 +194,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Parens_And_Namespace_But_No_Owner() { var target = new PropertyParser(); - var reader = new Reader("(foo:Bar)"); + var reader = new CharacterReader("(foo:Bar)"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(1, ex.Column); @@ -204,7 +205,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Missing_Close_Parens() { var target = new PropertyParser(); - var reader = new Reader("(Foo.Bar"); + var reader = new CharacterReader("(Foo.Bar"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(8, ex.Column); @@ -215,7 +216,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.Parsers public void Fails_With_Unexpected_Close_Parens() { var target = new PropertyParser(); - var reader = new Reader("Foo.Bar)"); + var reader = new CharacterReader("Foo.Bar)"); var ex = Assert.Throws(() => target.Parse(reader)); Assert.Equal(7, ex.Column);