Explorar o código

Merge branch 'master' into fixes/1764-attached-property-setter

danwalmsley %!s(int64=7) %!d(string=hai) anos
pai
achega
59f7b740ac
Modificáronse 27 ficheiros con 620 adicións e 370 borrados
  1. 28 37
      src/Avalonia.Animation/Animation.cs
  2. 59 7
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  3. 100 88
      src/Avalonia.Animation/AnimatorStateMachine`1.cs
  4. 25 50
      src/Avalonia.Animation/Animator`1.cs
  5. 1 1
      src/Avalonia.Animation/Cue.cs
  6. 4 4
      src/Avalonia.Animation/DoubleAnimator.cs
  7. 1 1
      src/Avalonia.Animation/IAnimationSetter.cs
  8. 11 5
      src/Avalonia.Animation/KeyFrame.cs
  9. 4 4
      src/Avalonia.Animation/KeyFramePair`1.cs
  10. 13 38
      src/Avalonia.Base/AvaloniaObject.cs
  11. 17 8
      src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs
  12. 3 0
      src/Avalonia.Base/IPriorityValueOwner.cs
  13. 8 3
      src/Avalonia.Base/PriorityValue.cs
  14. 34 19
      src/Avalonia.Base/Utilities/DeferredSetter.cs
  15. 58 0
      src/Avalonia.Base/Utilities/SingleOrQueue.cs
  16. 17 7
      src/Avalonia.Base/ValueStore.cs
  17. 39 51
      src/Avalonia.Controls/ProgressBar.cs
  18. 1 0
      src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs
  19. 3 3
      src/Avalonia.Styling/Styling/Setter.cs
  20. 36 9
      src/Avalonia.Themes.Default/ProgressBar.xaml
  21. 16 7
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs
  22. 2 2
      src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs
  23. 4 1
      src/Markup/Avalonia.Markup/Data/Binding.cs
  24. 2 2
      src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs
  25. 30 22
      tests/Avalonia.Base.UnitTests/PriorityValueTests.cs
  26. 50 0
      tests/Avalonia.Base.UnitTests/Utilities/SingleOrQueueTests.cs
  27. 54 1
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

+ 28 - 37
src/Avalonia.Animation/Animation.cs

@@ -68,7 +68,7 @@ namespace Avalonia.Animation
         /// <summary>
         /// The value fill mode for this animation.
         /// </summary>
-        public FillMode FillMode { get; set; } 
+        public FillMode FillMode { get; set; }
 
         /// <summary>
         /// Easing function to be used.
@@ -80,10 +80,10 @@ namespace Avalonia.Animation
             this.CollectionChanged += delegate { _isChildrenChanged = true; };
         }
  
-        private void InterpretKeyframes()
+        private IList<IAnimator> InterpretKeyframes(Animatable control)
         {
-            var handlerList = new List<(Type, AvaloniaProperty)>();
-            var kfList = new List<AnimatorKeyFrame>();
+            var handlerList = new List<(Type type, AvaloniaProperty property)>();
+            var animatorKeyFrames = new List<AnimatorKeyFrame>();
 
             foreach (var keyframe in this)
             {
@@ -99,41 +99,38 @@ namespace Avalonia.Animation
                     if (!handlerList.Contains((handler, setter.Property)))
                         handlerList.Add((handler, setter.Property));
 
-                    var newKF = new AnimatorKeyFrame()
+                    var cue = keyframe.Cue;
+
+                    if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
                     {
-                        Handler = handler,
-                        Property = setter.Property,
-                        Cue = keyframe.Cue,
-                        KeyTime = keyframe.KeyTime,
-                        timeSpanSet = keyframe.timeSpanSet,
-                        cueSet = keyframe.cueSet,
-                        Value = setter.Value
-                    };
-
-                    kfList.Add(newKF);
+                        cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
+                    }
+
+                    var newKF = new AnimatorKeyFrame(handler, cue);
+
+                    _subscription.Add(newKF.BindSetter(setter, control));
+
+                    animatorKeyFrames.Add(newKF);
                 }
             }
 
-            var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>();
+            var newAnimatorInstances = new List<IAnimator>();
 
-            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(newInstance);
             }
 
-            foreach (var kf in kfList)
+            foreach (var keyframe in animatorKeyFrames)
             {
-                var parent = newAnimatorInstances.Where(p => p.handler == kf.Handler &&
-                                                             p.prop == kf.Property)
-                                                 .First();
-                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);
-
+            return newAnimatorInstances;
         }
 
         /// <summary>
@@ -150,17 +147,11 @@ namespace Avalonia.Animation
         /// <inheritdocs/>
         public IDisposable Apply(Animatable control, IObservable<bool> 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;
         }
     }
-}
+}

+ 59 - 7
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,13 +13,63 @@ namespace Avalonia.Animation
     /// Defines a KeyFrame that is used for
     /// <see cref="Animator{T}"/> objects.
     /// </summary>
-    public class AnimatorKeyFrame
+    public class AnimatorKeyFrame : AvaloniaObject
     {
-        public Type Handler;
-        public Cue Cue;
-        public TimeSpan KeyTime;
-        internal bool timeSpanSet, cueSet;
-        public AvaloniaProperty Property;
-        public object Value;
+        public static readonly DirectProperty<AnimatorKeyFrame, object> ValueProperty =
+            AvaloniaProperty.RegisterDirect<AnimatorKeyFrame, object>(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, Animatable targetControl)
+        {
+            Property = setter.Property;
+            var value = setter.Value;
+
+            if (value is IBinding binding)
+            {
+                return this.Bind(ValueProperty, binding, targetControl);
+            }
+            else
+            {
+                return this.Bind(ValueProperty, ObservableEx.SingleValue(value).ToBinding(), targetControl);
+            }
+        }
+
+        public T GetTypedValue<T>()
+        {
+            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));
+        }
     }
 }

+ 100 - 88
src/Avalonia.Animation/AnimatorStateMachine`1.cs

@@ -51,9 +51,9 @@ namespace Avalonia.Animation
             Disposed
         }
 
-        public void Initialize(Animation animation, Animatable control, Animator<T> keyframes)
+        public void Initialize(Animation animation, Animatable control, Animator<T> animator)
         {
-            _parent = keyframes;
+            _parent = animator;
             _targetAnimation = animation;
             _targetControl = control;
             _neutralValue = (T)_targetControl.GetValue(_parent.Property);
@@ -123,121 +123,133 @@ 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;
+                    default:
+                        handled = true;
+                        break;
+                }
             }
         }
 
@@ -253,4 +265,4 @@ namespace Avalonia.Animation
             _currentState = KeyFramesStates.Disposed;
         }
     }
-}
+}

+ 25 - 50
src/Avalonia.Animation/Animator`1.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Animation
         /// <summary>
         /// List of type-converted keyframes.
         /// </summary>
-        private Dictionary<double, (T, bool isNeutral)> _convertedKeyframes = new Dictionary<double, (T, bool)>();
+        private readonly SortedList<double, (AnimatorKeyFrame, bool isNeutral)> _convertedKeyframes = new SortedList<double, (AnimatorKeyFrame, bool)>();
 
         private bool _isVerfifiedAndConverted;
 
@@ -38,12 +38,11 @@ namespace Avalonia.Animation
         public virtual IDisposable Apply(Animation animation, Animatable control, IObservable<bool> obsMatch)
         {
             if (!_isVerfifiedAndConverted)
-                VerifyConvertKeyFrames(animation, typeof(T));
+                VerifyConvertKeyFrames();
 
             return obsMatch
-                .Where(p => p == true)
                 // 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);
@@ -60,8 +59,8 @@ namespace Avalonia.Animation
         /// <param name="t">The time parameter, relative to the total animation time</param>
         protected (double IntraKFTime, KeyFramePair<T> KFPair) GetKFPairAndIntraKFTime(double t)
         {
-            KeyValuePair<double, (T, bool)> firstCue, lastCue;
-            int kvCount = _convertedKeyframes.Count();
+            KeyValuePair<double, (AnimatorKeyFrame frame, bool isNeutral)> firstCue, lastCue;
+            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
@@ -89,7 +88,9 @@ namespace Avalonia.Animation
             double t0 = firstCue.Key;
             double t1 = lastCue.Key;
             var intraframeTime = (t - t0) / (t1 - t0);
-            return (intraframeTime, new KeyFramePair<T>(firstCue, lastCue));
+            var firstFrameData = (firstCue.Value.frame.GetTypedValue<T>(), firstCue.Value.isNeutral);
+            var lastFrameData = (lastCue.Value.frame.GetTypedValue<T>(), lastCue.Value.isNeutral);
+            return (intraframeTime, new KeyFramePair<T>(firstFrameData, lastFrameData));
         }
 
 
@@ -98,17 +99,14 @@ namespace Avalonia.Animation
         /// </summary>
         private IDisposable RunKeyFrames(Animation animation, Animatable control)
         {
-            var _kfStateMach = new AnimatorStateMachine<T>();
-            _kfStateMach.Initialize(animation, control, this);
+            var stateMachine = new AnimatorStateMachine<T>();
+            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);
         }
 
         /// <summary>
@@ -119,39 +117,19 @@ namespace Avalonia.Animation
         /// <summary>
         /// Verifies and converts keyframe values according to this class's target type.
         /// </summary>
-        private void VerifyConvertKeyFrames(Animation animation, Type type)
+        private void VerifyConvertKeyFrames()
         {
-            var typeConv = TypeDescriptor.GetConverter(type);
-
-            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, type);
-
-                Cue _normalizedCue = k.Cue;
-
-                if (k.timeSpanSet)
-                {
-                    _normalizedCue = new Cue(k.KeyTime.Ticks / animation.Duration.Ticks);
-                }
-
-                _convertedKeyframes.Add(_normalizedCue.CueValue, (convertedValue, false));
+                _convertedKeyframes.Add(keyframe.Cue.CueValue, (keyframe, false));
             }
 
-            SortKeyFrameCues(_convertedKeyframes);
+            AddNeutralKeyFramesIfNeeded();
             _isVerfifiedAndConverted = true;
 
         }
 
-        private void SortKeyFrameCues(Dictionary<double, (T, bool)> convertedValues)
+        private void AddNeutralKeyFramesIfNeeded()
         {
             bool hasStartKey, hasEndKey;
             hasStartKey = hasEndKey = false;
@@ -170,23 +148,20 @@ namespace Avalonia.Animation
             }
 
             if (!hasStartKey || !hasEndKey)
-                AddNeutralKeyFrames(hasStartKey, hasEndKey, _convertedKeyframes);
-
-            _convertedKeyframes = _convertedKeyframes.OrderBy(p => p.Key)
-                                                     .ToDictionary((k) => k.Key, (v) => v.Value);
+                AddNeutralKeyFrames(hasStartKey, hasEndKey);
         }
 
-        private void AddNeutralKeyFrames(bool hasStartKey, bool hasEndKey, Dictionary<double, (T, bool)> 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));
             }
         }
     }
-}
+}

+ 1 - 1
src/Avalonia.Animation/Cue.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Animation
     /// A Cue object for <see cref="KeyFrame"/>. 
     /// </summary>
     [TypeConverter(typeof(CueTypeConverter))]
-    public struct Cue : IEquatable<Cue>, IEquatable<double>
+    public readonly struct Cue : IEquatable<Cue>, IEquatable<double>
     {
         /// <summary>
         /// The normalized percent value, ranging from 0.0 to 1.0

+ 4 - 4
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);

+ 1 - 1
src/Avalonia.Animation/IAnimationSetter.cs

@@ -5,4 +5,4 @@ namespace Avalonia.Animation
         AvaloniaProperty Property { get; set; }
         object Value { get; set; }
     }
-}
+}

+ 11 - 5
src/Avalonia.Animation/KeyFrame.cs

@@ -7,6 +7,11 @@ using Avalonia.Collections;
 
 namespace Avalonia.Animation
 {
+    internal enum KeyFrameTimingMode
+    {
+        TimeSpan = 1,
+        Cue
+    }
 
     /// <summary>
     /// Stores data regarding a specific key
@@ -14,7 +19,6 @@ namespace Avalonia.Animation
     /// </summary>
     public class KeyFrame : AvaloniaList<IAnimationSetter>
     {
-        internal bool timeSpanSet, cueSet;
         private TimeSpan _ktimeSpan;
         private Cue _kCue;
 
@@ -30,6 +34,8 @@ namespace Avalonia.Animation
         {
         }
 
+        internal KeyFrameTimingMode TimingMode { get; private set; }
+
         /// <summary>
         /// Gets or sets the key time of this <see cref="KeyFrame"/>.
         /// </summary>
@@ -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;
             }
         }

+ 4 - 4
src/Avalonia.Animation/KeyFramePair`1.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Animation
         /// </summary>
         /// <param name="FirstKeyFrame"></param>
         /// <param name="LastKeyFrame"></param>
-        public KeyFramePair(KeyValuePair<double, (T, bool)> FirstKeyFrame, KeyValuePair<double, (T, bool)> 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
         /// <summary>
         /// First <see cref="KeyFrame"/> object.
         /// </summary>
-        public KeyValuePair<double, (T TargetValue, bool isNeutral)> FirstKeyFrame { get; private set; }
+        public (T TargetValue, bool isNeutral) FirstKeyFrame { get; }
 
         /// <summary>
         /// Second <see cref="KeyFrame"/> object.
         /// </summary>
-        public KeyValuePair<double, (T TargetValue, bool isNeutral)> SecondKeyFrame { get; private set; }
+        public (T TargetValue, bool isNeutral) SecondKeyFrame { get; }
     }
-}
+}

+ 13 - 38
src/Avalonia.Base/AvaloniaObject.cs

@@ -22,7 +22,7 @@ namespace Avalonia
     /// <remarks>
     /// This class is analogous to DependencyObject in WPF.
     /// </remarks>
-    public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner
+    public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged
     {
         /// <summary>
         /// The parent object that inherited values are inherited from.
@@ -45,21 +45,8 @@ namespace Avalonia
         /// </summary>
         private EventHandler<AvaloniaPropertyChangedEventArgs> _propertyChanged;
 
-        private DeferredSetter<AvaloniaProperty, object> _directDeferredSetter;
         private ValueStore _values;
-
-        /// <summary>
-        /// Delayed setter helper for direct properties. Used to fix #855.
-        /// </summary>
-        private DeferredSetter<AvaloniaProperty, object> DirectPropertyDeferredSetter
-        {
-            get
-            {
-                return _directDeferredSetter ??
-                    (_directDeferredSetter = new DeferredSetter<AvaloniaProperty, object>());
-            }
-        }
-
+        private ValueStore Values => _values ?? (_values = new ValueStore(this));
 
         /// <summary>
         /// Initializes a new instance of the <see cref="AvaloniaObject"/> class.
@@ -225,7 +212,7 @@ namespace Avalonia
             }
             else if (_values != null)
             {
-                var result = _values.GetValue(property);
+                var result = Values.GetValue(property);
 
                 if (result == AvaloniaProperty.UnsetValue)
                 {
@@ -376,12 +363,7 @@ namespace Avalonia
                     description,
                     priority);
 
-                if (_values == null)
-                {
-                    _values = new ValueStore(this);
-                }
-
-                return _values.AddBinding(property, source, priority);
+                return Values.AddBinding(property, source, priority);
             }
         }
 
@@ -414,9 +396,8 @@ namespace Avalonia
             VerifyAccess();
             _values?.Revalidate(property);
         }
-
-        /// <inheritdoc/>
-        void IPriorityValueOwner.Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
+        
+        internal void PriorityValueChanged(AvaloniaProperty property, int priority, object oldValue, object newValue)
         {
             oldValue = (oldValue == AvaloniaProperty.UnsetValue) ?
                 GetDefaultValue(property) :
@@ -439,9 +420,8 @@ namespace Avalonia
                     (BindingPriority)priority);
             }
         }
-
-        /// <inheritdoc/>
-        void IPriorityValueOwner.BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
+        
+        internal void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
         {
             UpdateDataValidation(property, notification);
         }
@@ -456,7 +436,7 @@ namespace Avalonia
         /// Gets all priority values set on the object.
         /// </summary>
         /// <returns>A collection of property/value tuples.</returns>
-        internal IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => _values?.GetSetValues();
+        internal IDictionary<AvaloniaProperty, object> GetSetValues() => Values?.GetSetValues();
 
         /// <summary>
         /// Forces revalidation of properties when a property value changes.
@@ -566,12 +546,12 @@ namespace Avalonia
             T value)
         {
             Contract.Requires<ArgumentNullException>(setterCallback != null);
-            return DirectPropertyDeferredSetter.SetAndNotify(
+            return Values.Setter.SetAndNotify(
                 property,
                 ref field,
-                (object val, ref T backing, Action<Action> notify) =>
+                (object update, ref T backing, Action<Action> notify) =>
                 {
-                    setterCallback((T)val, ref backing, notify);
+                    setterCallback((T)update, ref backing, notify);
                     return true;
                 },
                 value);
@@ -737,13 +717,8 @@ namespace Avalonia
                     originalValue?.GetType().FullName ?? "(null)"));
             }
 
-            if (_values == null)
-            {
-                _values = new ValueStore(this);
-            }
-
             LogPropertySet(property, value, priority);
-            _values.AddValue(property, value, (int)priority);
+            Values.AddValue(property, value, (int)priority);
         }
 
         /// <summary>

+ 17 - 8
src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs

@@ -23,15 +23,24 @@ namespace Avalonia.Diagnostics
         {
             var set = o.GetSetValues();
 
-            PriorityValue value;
-
-            if (set.TryGetValue(property, out value))
+            if (set.TryGetValue(property, out var obj))
             {
-                return new AvaloniaPropertyValue(
-                    property,
-                    o.GetValue(property),
-                    (BindingPriority)value.ValuePriority,
-                    value.GetDiagnostic());
+                if (obj is PriorityValue value)
+                {
+                    return new AvaloniaPropertyValue(
+                        property,
+                        o.GetValue(property),
+                        (BindingPriority)value.ValuePriority,
+                        value.GetDiagnostic());
+                }
+                else
+                {
+                    return new AvaloniaPropertyValue(
+                        property,
+                        obj,
+                        BindingPriority.LocalValue,
+                        "Local value");
+                }
             }
             else
             {

+ 3 - 0
src/Avalonia.Base/IPriorityValueOwner.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;
+using Avalonia.Utilities;
 
 namespace Avalonia
 {
@@ -31,5 +32,7 @@ namespace Avalonia
         /// Ensures that the current thread is the UI thread.
         /// </summary>
         void VerifyAccess();
+
+        DeferredSetter<object> Setter { get; }
     }
 }

+ 8 - 3
src/Avalonia.Base/PriorityValue.cs

@@ -21,7 +21,7 @@ namespace Avalonia
     /// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. Where there
     /// are multiple bindings registered with the same priority, the most recently added binding
     /// has a higher priority. Each time the value changes, the 
-    /// <see cref="IPriorityValueOwner.Changed(PriorityValue, object, object)"/> method on the 
+    /// <see cref="IPriorityValueOwner.Changed"/> method on the 
     /// owner object is fired with the old and new values.
     /// </remarks>
     internal class PriorityValue
@@ -30,7 +30,6 @@ namespace Avalonia
         private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
 
         private readonly Func<object, object> _validate;
-        private static readonly DeferredSetter<PriorityValue, (object value, int priority)> delayedSetter = new DeferredSetter<PriorityValue, (object, int)>();
         private (object value, int priority) _value;
 
         /// <summary>
@@ -243,12 +242,18 @@ namespace Avalonia
         /// <param name="priority">The priority level that the value came from.</param>
         private void UpdateValue(object value, int priority)
         {
-            delayedSetter.SetAndNotify(this,
+            Owner.Setter.SetAndNotify(Property,
                 ref _value,
                 UpdateCore,
                 (value, priority));
         }
 
+        private bool UpdateCore(
+            object update,
+            ref (object value, int priority) backing,
+            Action<Action> notify)
+            => UpdateCore(((object, int))update, ref backing, notify);
+
         private bool UpdateCore(
             (object value, int priority) update,
             ref (object value, int priority) backing,

+ 34 - 19
src/Avalonia.Base/Utilities/DeferredSetter.cs

@@ -8,11 +8,10 @@ namespace Avalonia.Utilities
 {
     /// <summary>
     /// A utility class to enable deferring assignment until after property-changed notifications are sent.
+    /// Used to fix #855.
     /// </summary>
-    /// <typeparam name="TProperty">The type of the object that represents the property.</typeparam>
     /// <typeparam name="TSetRecord">The type of value with which to track the delayed assignment.</typeparam>
-    class DeferredSetter<TProperty, TSetRecord>
-        where TProperty: class
+    class DeferredSetter<TSetRecord>
     {
         private struct NotifyDisposable : IDisposable
         {
@@ -37,29 +36,44 @@ namespace Avalonia.Utilities
         {
             public bool Notifying { get; set; }
 
-            private Queue<TSetRecord> pendingValues;
+            private SingleOrQueue<TSetRecord> pendingValues;
             
-            public Queue<TSetRecord> PendingValues
+            public SingleOrQueue<TSetRecord> PendingValues
             {
                 get
                 {
-                    return pendingValues ?? (pendingValues = new Queue<TSetRecord>());
+                    return pendingValues ?? (pendingValues = new SingleOrQueue<TSetRecord>());
                 }
             }
         }
 
-        private readonly ConditionalWeakTable<TProperty, SettingStatus> setRecords = new ConditionalWeakTable<TProperty, SettingStatus>();
+        private Dictionary<AvaloniaProperty, SettingStatus> _setRecords;
+        private Dictionary<AvaloniaProperty, SettingStatus> SetRecords
+            => _setRecords ?? (_setRecords = new Dictionary<AvaloniaProperty, SettingStatus>());
+
+        private SettingStatus GetOrCreateStatus(AvaloniaProperty property)
+        {
+            if (!SetRecords.TryGetValue(property, out var status))
+            {
+                status = new SettingStatus();
+                SetRecords.Add(property, status);
+            }
+
+            return status;
+        }
 
         /// <summary>
         /// Mark the property as currently notifying.
         /// </summary>
         /// <param name="property">The property to mark as notifying.</param>
         /// <returns>Returns a disposable that when disposed, marks the property as done notifying.</returns>
-        private NotifyDisposable MarkNotifying(TProperty property)
+        private NotifyDisposable MarkNotifying(AvaloniaProperty property)
         {
             Contract.Requires<InvalidOperationException>(!IsNotifying(property));
-            
-            return new NotifyDisposable(setRecords.GetOrCreateValue(property));
+
+            SettingStatus status = GetOrCreateStatus(property);
+
+            return new NotifyDisposable(status);
         }
 
         /// <summary>
@@ -67,19 +81,19 @@ namespace Avalonia.Utilities
         /// </summary>
         /// <param name="property">The property.</param>
         /// <returns>If the property is currently notifying listeners.</returns>
-        private bool IsNotifying(TProperty property)
-            => setRecords.TryGetValue(property, out var value) && value.Notifying;
+        private bool IsNotifying(AvaloniaProperty property)
+            => SetRecords.TryGetValue(property, out var value) && value.Notifying;
 
         /// <summary>
         /// Add a pending assignment for the property.
         /// </summary>
         /// <param name="property">The property.</param>
         /// <param name="value">The value to assign.</param>
-        private void AddPendingSet(TProperty property, TSetRecord value)
+        private void AddPendingSet(AvaloniaProperty property, TSetRecord value)
         {
             Contract.Requires<InvalidOperationException>(IsNotifying(property));
 
-            setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value);
+            GetOrCreateStatus(property).PendingValues.Enqueue(value);
         }
 
         /// <summary>
@@ -87,9 +101,9 @@ namespace Avalonia.Utilities
         /// </summary>
         /// <param name="property">The property to check.</param>
         /// <returns>If the property has any pending assignments.</returns>
-        private bool HasPendingSet(TProperty property)
+        private bool HasPendingSet(AvaloniaProperty property)
         {
-            return setRecords.TryGetValue(property, out var status) && status.PendingValues.Count != 0;
+            return SetRecords.TryGetValue(property, out var status) && !status.PendingValues.Empty;
         }
 
         /// <summary>
@@ -97,9 +111,9 @@ namespace Avalonia.Utilities
         /// </summary>
         /// <param name="property">The property to check.</param>
         /// <returns>The first pending assignment for the property.</returns>
-        private TSetRecord GetFirstPendingSet(TProperty property)
+        private TSetRecord GetFirstPendingSet(AvaloniaProperty property)
         {
-            return setRecords.GetOrCreateValue(property).PendingValues.Dequeue();
+            return GetOrCreateStatus(property).PendingValues.Dequeue();
         }
 
         public delegate bool SetterDelegate<TValue>(TSetRecord record, ref TValue backing, Action<Action> notifyCallback);
@@ -115,7 +129,7 @@ namespace Avalonia.Utilities
         /// </param>
         /// <param name="value">The value to try to set.</param>
         public bool SetAndNotify<TValue>(
-            TProperty property,
+            AvaloniaProperty property,
             ref TValue backing,
             SetterDelegate<TValue> setterCallback,
             TSetRecord value)
@@ -144,6 +158,7 @@ namespace Avalonia.Utilities
                         }
                     });
                 }
+
                 return updated;
             }
             else if(!object.Equals(value, backing))

+ 58 - 0
src/Avalonia.Base/Utilities/SingleOrQueue.cs

@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Avalonia.Utilities
+{
+    /// <summary>
+    /// FIFO Queue optimized for holding zero or one items.
+    /// </summary>
+    /// <typeparam name="T">The type of items held in the queue.</typeparam>
+    public class SingleOrQueue<T>
+    {
+        private T _head;
+        private Queue<T> _tail;
+
+        private Queue<T> Tail => _tail ?? (_tail = new Queue<T>());
+
+        private bool HasTail => _tail != null;
+
+        public bool Empty { get; private set; } = true;
+
+        public void Enqueue(T value)
+        {
+            if (Empty)
+            {
+                _head = value;
+            }
+            else
+            {
+                Tail.Enqueue(value);
+            }
+
+            Empty = false;
+        }
+
+        public T Dequeue()
+        {
+            if (Empty)
+            {
+                throw new InvalidOperationException("Cannot dequeue from an empty queue!");
+            }
+
+            var result = _head;
+
+            if (HasTail && Tail.Count != 0)
+            {
+                _head = Tail.Dequeue();
+            }
+            else
+            {
+                _head = default;
+                Empty = true;
+            }
+
+            return result;
+        }
+    }
+}

+ 17 - 7
src/Avalonia.Base/ValueStore.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using Avalonia.Data;
+using Avalonia.Utilities;
 
 namespace Avalonia
 {
@@ -91,15 +92,15 @@ namespace Avalonia
 
         public void BindingNotificationReceived(AvaloniaProperty property, BindingNotification notification)
         {
-            ((IPriorityValueOwner)_owner).BindingNotificationReceived(property, notification);
+            _owner.BindingNotificationReceived(property, notification);
         }
 
         public void Changed(AvaloniaProperty property, int priority, object oldValue, object newValue)
         {
-            ((IPriorityValueOwner)_owner).Changed(property, priority, oldValue, newValue);
+            _owner.PriorityValueChanged(property, priority, oldValue, newValue);
         }
 
-        public IDictionary<AvaloniaProperty, PriorityValue> GetSetValues() => throw new NotImplementedException();
+        public IDictionary<AvaloniaProperty, object> GetSetValues() => _values;
 
         public object GetValue(AvaloniaProperty property)
         {
@@ -115,7 +116,7 @@ namespace Avalonia
 
         public bool IsAnimating(AvaloniaProperty property)
         {
-            return _values.TryGetValue(property, out var value) ? (value as PriorityValue)?.IsAnimating ?? false : false;
+            return _values.TryGetValue(property, out var value) && value is PriorityValue priority && priority.IsAnimating;
         }
 
         public bool IsSet(AvaloniaProperty property)
@@ -148,13 +149,11 @@ namespace Avalonia
                 validate2 = v => validate(_owner, v);
             }
 
-            PriorityValue result = new PriorityValue(
+            return new PriorityValue(
                 this,
                 property,
                 property.PropertyType,
                 validate2);
-
-            return result;
         }
 
         private object Validate(AvaloniaProperty property, object value)
@@ -168,5 +167,16 @@ namespace Avalonia
 
             return value;
         }
+
+        private DeferredSetter<object> _defferedSetter;
+
+        public DeferredSetter<object> Setter
+        {
+            get
+            {
+                return _defferedSetter ??
+                    (_defferedSetter = new DeferredSetter<object>());
+            }
+        }
     }
 }

+ 39 - 51
src/Avalonia.Controls/ProgressBar.cs

@@ -21,18 +21,27 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<Orientation> OrientationProperty =
             AvaloniaProperty.Register<ProgressBar, Orientation>(nameof(Orientation), Orientation.Horizontal);
 
+        private static readonly DirectProperty<ProgressBar, double> IndeterminateStartingOffsetProperty =
+            AvaloniaProperty.RegisterDirect<ProgressBar, double>(
+                nameof(IndeterminateStartingOffset),
+                p => p.IndeterminateStartingOffset,
+                (p, o) => p.IndeterminateStartingOffset = o);
+
+        private static readonly DirectProperty<ProgressBar, double> IndeterminateEndingOffsetProperty =
+            AvaloniaProperty.RegisterDirect<ProgressBar, double>(
+                nameof(IndeterminateEndingOffset),
+                p => p.IndeterminateEndingOffset,
+                (p, o) => p.IndeterminateEndingOffset = o);
+
         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<ProgressBar>(x => x.ValueChanged);
-
-            IsIndeterminateProperty.Changed.AddClassHandler<ProgressBar>(
-                (p, e) => { if (p._indicator != null) p.UpdateIsIndeterminate((bool)e.NewValue); });
         }
 
         public bool IsIndeterminate
@@ -46,6 +55,19 @@ namespace Avalonia.Controls
             get => GetValue(OrientationProperty);
             set => SetValue(OrientationProperty, value);
         }
+        private double _indeterminateStartingOffset;
+        private double IndeterminateStartingOffset
+        {
+            get => _indeterminateStartingOffset;
+            set => SetAndRaise(IndeterminateStartingOffsetProperty, ref _indeterminateStartingOffset, value);
+        }
+
+        private double _indeterminateEndingOffset;
+        private double IndeterminateEndingOffset
+        {
+            get => _indeterminateEndingOffset;
+            set => SetAndRaise(IndeterminateEndingOffsetProperty, ref _indeterminateEndingOffset, value);
+        }
 
         /// <inheritdoc/>
         protected override Size ArrangeOverride(Size finalSize)
@@ -60,7 +82,6 @@ namespace Avalonia.Controls
             _indicator = e.NameScope.Get<Border>("PART_Indicator");
 
             UpdateIndicator(Bounds.Size);
-            UpdateIsIndeterminate(IsIndeterminate);
         }
 
         private void UpdateIndicator(Size bounds)
@@ -70,9 +91,20 @@ 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;
+                        IndeterminateEndingOffset = bounds.Width;
+
+                    }
                     else
-                        _indicator.Height = bounds.Height / 5.0;
+                    {
+                        var height = bounds.Height / 5.0;
+                        IndeterminateStartingOffset = -bounds.Height;
+                        _indicator.Height = height;
+                        IndeterminateEndingOffset = height;
+                    }
                 }
                 else
                 {
@@ -86,53 +118,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> _progressBar;
-
-            private bool _disposed;
-
-            public bool Disposed => _disposed;
-
-            private IndeterminateAnimation(ProgressBar progressBar)
-            {
-                _progressBar = new WeakReference<ProgressBar>(progressBar);
-
-            }
-
-            public static IndeterminateAnimation StartAnimation(ProgressBar progressBar)
-            {
-                return new IndeterminateAnimation(progressBar);
-            }
-
-            private Rect GetAnimationRect(TimeSpan time)
-            {
-                return Rect.Empty;
-            }
-
-            public void Dispose()
-            {
-                _disposed = true;
-            }
-        }
     }
 }

+ 1 - 0
src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs

@@ -31,6 +31,7 @@ namespace Avalonia.Diagnostics.ViewModels
                 }
             };
 
+            SelectedTab = 0;
             root.GetObservable(TopLevel.PointerOverElementProperty)
                 .Subscribe(x => PointerOverElement = x?.GetType().Name);
         }

+ 3 - 3
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<bool> activator)
+        private InstancedBinding Clone(InstancedBinding sourceInstance, BindingMode mode, IStyle style, IObservable<bool> activator)
         {
             if (activator != null)
             {
                 var description = style?.ToString();
 
-                switch (sourceInstance.Mode)
+                switch (mode)
                 {
                     case BindingMode.OneTime:
                         if (sourceInstance.Observable != null)

+ 36 - 9
src/Avalonia.Themes.Default/ProgressBar.xaml

@@ -7,14 +7,9 @@
         <Border Background="{TemplateBinding Background}"
                 BorderBrush="{TemplateBinding BorderBrush}"
                 BorderThickness="{TemplateBinding BorderThickness}">
-          <Grid>
-            <Border Name="PART_Track"
-                    BorderThickness="1"
-                    BorderBrush="{TemplateBinding Background}"/>
-            <Border Name="PART_Indicator"
-                    BorderThickness="1"
-                    Background="{TemplateBinding Foreground}" />
-          </Grid>
+          <Border Name="PART_Indicator"
+                  BorderThickness="1"
+                  Background="{TemplateBinding Foreground}"/>
         </Border>
       </ControlTemplate>
     </Setter>
@@ -35,4 +30,36 @@
     <Setter Property="MinWidth" Value="14"/>
     <Setter Property="MinHeight" Value="200"/>
   </Style>
-</Styles>
+  <Style Selector="ProgressBar:horizontal:indeterminate /template/ Border#PART_Indicator">
+      <Style.Animations>
+          <Animation Duration="0:0:3"
+                     RepeatCount="Loop"
+                     Easing="LinearEasing">
+              <KeyFrame Cue="0%">
+                  <Setter Property="TranslateTransform.X"
+                          Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+              </KeyFrame>
+              <KeyFrame Cue="100%">
+                  <Setter Property="TranslateTransform.X"
+                          Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+              </KeyFrame>
+      </Animation>
+      </Style.Animations>
+  </Style>
+  <Style Selector="ProgressBar:vertical:indeterminate /template/ Border#PART_Indicator">
+      <Style.Animations>
+          <Animation Duration="0:0:3"
+                     RepeatCount="Loop"
+                     Easing="LinearEasing">
+              <KeyFrame Cue="0%">
+                  <Setter Property="TranslateTransform.Y"
+                          Value="{Binding IndeterminateStartingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+              </KeyFrame>
+              <KeyFrame Cue="100%">
+                  <Setter Property="TranslateTransform.Y"
+                          Value="{Binding IndeterminateEndingOffset, RelativeSource={RelativeSource TemplatedParent}}" />
+              </KeyFrame>
+      </Animation>
+      </Style.Animations>
+  </Style>
+</Styles>

+ 16 - 7
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs

@@ -87,19 +87,28 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
             if (string.IsNullOrWhiteSpace(path) || path == ".")
             {
                 result.Path = string.Empty;
+                return result;
             }
-            else if (path.StartsWith("#"))
+            else if (path.StartsWith("!"))
+            {
+                int pathStart = 0;
+                for (; pathStart < path.Length && path[pathStart] == '!'; ++pathStart);
+                result.Path = path.Substring(0, pathStart);
+                path = path.Substring(pathStart);
+            }
+
+            if (path.StartsWith("#"))
             {
                 var dot = path.IndexOf('.');
 
                 if (dot != -1)
                 {
-                    result.Path = path.Substring(dot + 1);
+                    result.Path += path.Substring(dot + 1);
                     result.ElementName = path.Substring(1, dot - 1);
                 }
                 else
                 {
-                    result.Path = string.Empty;
+                    result.Path += string.Empty;
                     result.ElementName = path.Substring(1);
                 }
             }
@@ -114,12 +123,12 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
                 string relativeSourceMode;
                 if (dot != -1)
                 {
-                    result.Path = path.Substring(dot + 1);
+                    result.Path += path.Substring(dot + 1);
                     relativeSourceMode = path.Substring(1, dot - 1);
                 }
                 else
                 {
-                    result.Path = string.Empty;
+                    result.Path += string.Empty;
                     relativeSourceMode = path.Substring(1);
                 }
 
@@ -170,7 +179,7 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
             }
             else
             {
-                result.Path = path;
+                result.Path += path;
             }
 
             return result;
@@ -229,4 +238,4 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions
 
         public RelativeSource RelativeSource { get; set; }
     }
-}
+}

+ 2 - 2
src/Markup/Avalonia.Markup.Xaml/PortableXaml/AvaloniaRuntimeTypeProvider.cs

@@ -7,7 +7,6 @@ using System.Linq;
 using System.Reflection;
 using Avalonia.Controls;
 using Avalonia.Data;
-using Avalonia.Markup.Data;
 using Avalonia.Markup.Xaml.Templates;
 using Avalonia.Media;
 using Avalonia.Metadata;
@@ -33,6 +32,7 @@ namespace Avalonia.Markup.Xaml.Context
         private static readonly IEnumerable<Assembly> ForcedAssemblies = new[]
         {
             typeof(AvaloniaObject).GetTypeInfo().Assembly,
+            typeof(Animation.Animation).GetTypeInfo().Assembly,
             typeof(Control).GetTypeInfo().Assembly,
             typeof(Style).GetTypeInfo().Assembly,
             typeof(DataTemplate).GetTypeInfo().Assembly,
@@ -146,4 +146,4 @@ namespace Avalonia.Markup.Xaml.Context
             return null;
         }
     }
-}
+}

+ 4 - 1
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)
             {

+ 2 - 2
src/OSX/Avalonia.MonoMac/SystemDialogsImpl.cs

@@ -25,9 +25,9 @@ namespace Avalonia.MonoMac
                 else
                 {
                     if (panel is NSOpenPanel openPanel)
-                        tcs.SetResult(openPanel.Urls.Select(url => url.AbsoluteString).ToArray());
+                        tcs.SetResult(openPanel.Urls.Select(url => url.AbsoluteString.Replace("file://", "")).ToArray());
                     else
-                        tcs.SetResult(new[] { panel.Url.AbsoluteString });
+                        tcs.SetResult(new[] { panel.Url.AbsoluteString.Replace("file://", "") });
                 }
                 panel.OrderOut(panel);
                 keyWindow?.MakeKeyAndOrderFront(keyWindow);

+ 30 - 22
tests/Avalonia.Base.UnitTests/PriorityValueTests.cs

@@ -1,11 +1,12 @@
 // Copyright (c) The Avalonia Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using Avalonia.Utilities;
+using Moq;
 using System;
 using System.Linq;
 using System.Reactive.Linq;
 using System.Reactive.Subjects;
-using Moq;
 using Xunit;
 
 namespace Avalonia.Base.UnitTests
@@ -21,7 +22,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Initial_Value_Should_Be_UnsetValue()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
 
             Assert.Same(AvaloniaProperty.UnsetValue, target.Value);
         }
@@ -29,7 +30,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void First_Binding_Sets_Value()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
 
             target.Add(Single("foo"), 0);
 
@@ -39,7 +40,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Changing_Binding_Should_Set_Value()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
             var subject = new BehaviorSubject<string>("foo");
 
             target.Add(subject, 0);
@@ -51,7 +52,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Setting_Direct_Value_Should_Override_Binding()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
 
             target.Add(Single("foo"), 0);
             target.SetValue("bar", 0);
@@ -62,7 +63,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Binding_Firing_Should_Override_Direct_Value()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
             var source = new BehaviorSubject<object>("initial");
 
             target.Add(source, 0);
@@ -76,7 +77,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Earlier_Binding_Firing_Should_Not_Override_Later()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
             var nonActive = new BehaviorSubject<object>("na");
             var source = new BehaviorSubject<object>("initial");
 
@@ -92,7 +93,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Binding_Completing_Should_Revert_To_Direct_Value()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
             var source = new BehaviorSubject<object>("initial");
 
             target.Add(source, 0);
@@ -108,7 +109,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Binding_With_Lower_Priority_Has_Precedence()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
 
             target.Add(Single("foo"), 1);
             target.Add(Single("bar"), 0);
@@ -120,7 +121,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Later_Binding_With_Same_Priority_Should_Take_Precedence()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
 
             target.Add(Single("foo"), 1);
             target.Add(Single("bar"), 0);
@@ -133,7 +134,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Changing_Binding_With_Lower_Priority_Should_Set_Not_Value()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
             var subject = new BehaviorSubject<string>("bar");
 
             target.Add(Single("foo"), 0);
@@ -146,7 +147,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void UnsetValue_Should_Fall_Back_To_Next_Binding()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
             var subject = new BehaviorSubject<object>("bar");
 
             target.Add(subject, 0);
@@ -162,7 +163,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Adding_Value_Should_Call_OnNext()
         {
-            var owner = new Mock<IPriorityValueOwner>();
+            var owner = GetMockOwner();
             var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
 
             target.Add(Single("foo"), 0);
@@ -173,7 +174,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Changing_Value_Should_Call_OnNext()
         {
-            var owner = new Mock<IPriorityValueOwner>();
+            var owner = GetMockOwner();
             var target = new PriorityValue(owner.Object, TestProperty, typeof(string));
             var subject = new BehaviorSubject<object>("foo");
 
@@ -186,7 +187,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Disposing_A_Binding_Should_Revert_To_Next_Value()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
 
             target.Add(Single("foo"), 0);
             var disposable = target.Add(Single("bar"), 0);
@@ -199,7 +200,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Disposing_A_Binding_Should_Remove_BindingEntry()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
 
             target.Add(Single("foo"), 0);
             var disposable = target.Add(Single("bar"), 0);
@@ -212,7 +213,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Completing_A_Binding_Should_Revert_To_Previous_Binding()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
             var source = new BehaviorSubject<object>("bar");
 
             target.Add(Single("foo"), 0);
@@ -226,7 +227,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Completing_A_Binding_Should_Revert_To_Lower_Priority()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
             var source = new BehaviorSubject<object>("bar");
 
             target.Add(Single("foo"), 1);
@@ -240,7 +241,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Completing_A_Binding_Should_Remove_BindingEntry()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(string));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(string));
             var subject = new BehaviorSubject<object>("bar");
 
             target.Add(Single("foo"), 0);
@@ -254,7 +255,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Direct_Value_Should_Be_Coerced()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, 10));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
 
             target.SetValue(5, 0);
             Assert.Equal(5, target.Value);
@@ -265,7 +266,7 @@ namespace Avalonia.Base.UnitTests
         [Fact]
         public void Bound_Value_Should_Be_Coerced()
         {
-            var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, 10));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, 10));
             var source = new Subject<object>();
 
             target.Add(source, 0);
@@ -279,7 +280,7 @@ namespace Avalonia.Base.UnitTests
         public void Revalidate_Should_ReCoerce_Value()
         {
             var max = 10;
-            var target = new PriorityValue(null, TestProperty, typeof(int), x => Math.Min((int)x, max));
+            var target = new PriorityValue(GetMockOwner().Object, TestProperty, typeof(int), x => Math.Min((int)x, max));
             var source = new Subject<object>();
 
             target.Add(source, 0);
@@ -302,5 +303,12 @@ namespace Avalonia.Base.UnitTests
         {
             return Observable.Never<T>().StartWith(value);
         }
+
+        private static Mock<IPriorityValueOwner> GetMockOwner()
+        {
+            var owner = new Mock<IPriorityValueOwner>();
+            owner.SetupGet(o => o.Setter).Returns(new DeferredSetter<object>());
+            return owner;
+        }
     }
 }

+ 50 - 0
tests/Avalonia.Base.UnitTests/Utilities/SingleOrQueueTests.cs

@@ -0,0 +1,50 @@
+using Avalonia.Utilities;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Xunit;
+
+namespace Avalonia.Base.UnitTests.Utilities
+{
+    public class SingleOrQueueTests
+    {
+        [Fact]
+        public void New_SingleOrQueue_Is_Empty()
+        {
+            Assert.True(new SingleOrQueue<object>().Empty);
+        }
+
+        [Fact]
+        public void Dequeue_Throws_When_Empty()
+        {
+            var queue = new SingleOrQueue<object>();
+
+            Assert.Throws<InvalidOperationException>(() => queue.Dequeue());
+        }
+
+        [Fact]
+        public void Enqueue_Adds_Element()
+        {
+            var queue = new SingleOrQueue<int>();
+
+            queue.Enqueue(1);
+
+            Assert.False(queue.Empty);
+
+            Assert.Equal(1, queue.Dequeue());
+        }
+
+        [Fact]
+        public void Multiple_Elements_Dequeued_In_Correct_Order()
+        {
+            var queue = new SingleOrQueue<int>();
+
+            queue.Enqueue(1);
+            queue.Enqueue(2);
+            queue.Enqueue(3);
+            Assert.Equal(1, queue.Dequeue());
+            Assert.Equal(2, queue.Dequeue());
+            Assert.Equal(3, queue.Dequeue());
+        }
+    }
+}

+ 54 - 1
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs

@@ -297,7 +297,60 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
                 Assert.Equal("title", button.Content);
             }
         }
+
+        [Fact]
+        public void Shorthand_Binding_With_Negation_Works()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <Border Name='border1'>
+      <Border Name='border2'>
+        <Button Name='button' Content='{Binding !$self.IsDefault}'/>
+      </Border>
+    </Border>
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                window.ApplyTemplate();
+
+#pragma warning disable xUnit2004 // Diagnostic mis-firing since button.Content isn't guaranteed to be a bool.
+                Assert.Equal(true, button.Content);
+#pragma warning restore xUnit2004
+            }
+        }
+        [Fact]
+        public void Shorthand_Binding_With_Multiple_Negation_Works()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+        xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'>
+    <Border Name='border1'>
+      <Border Name='border2'>
+        <Button Name='button' Content='{Binding !!$self.IsDefault}'/>
+      </Border>
+    </Border>
+</Window>";
+                var loader = new AvaloniaXamlLoader();
+                var window = (Window)loader.Load(xaml);
+                var button = window.FindControl<Button>("button");
+
+                window.ApplyTemplate();
+
+#pragma warning disable xUnit2004 // Diagnostic mis-firing since button.Content isn't guaranteed to be a bool.
+                Assert.Equal(false, button.Content);
+#pragma warning restore xUnit2004
+            }
+        }
     }
 
     public class TestWindow : Window { }
-}
+}