diff --git a/.gitignore b/.gitignore index 583a2b8a2b..32acee4c90 100644 --- a/.gitignore +++ b/.gitignore @@ -165,6 +165,11 @@ $RECYCLE.BIN/ ################# .idea +################# +## VS Code +################# +.vscode/ + ################# ## Cake ################# diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index a69fb75742..95713dc22f 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -18,18 +18,18 @@ - + - + - + !BooleanString @@ -37,13 +37,13 @@ - + - + @@ -52,7 +52,7 @@ - + @@ -68,11 +68,11 @@ - + - + @@ -87,16 +87,16 @@ - + - + - + @@ -104,7 +104,7 @@ - + @@ -25,7 +25,7 @@ - + @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml index 1797fb48bc..fba15f6e77 100644 --- a/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonSpinnerPage.xaml @@ -1,11 +1,11 @@  - + ButtonSpinner The ButtonSpinner control allows you to add button spinners to any element and then respond to the Spin event to manipulate that element. - + AllowSpin ShowButtonSpinner - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CalendarPage.xaml b/samples/ControlCatalog/Pages/CalendarPage.xaml index a433fd1add..c47fd766fb 100644 --- a/samples/ControlCatalog/Pages/CalendarPage.xaml +++ b/samples/ControlCatalog/Pages/CalendarPage.xaml @@ -1,13 +1,13 @@ - + Calendar A calendar control for selecting dates + Spacing="16"> - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CanvasPage.xaml b/samples/ControlCatalog/Pages/CanvasPage.xaml index f934f57c22..10a38895a2 100644 --- a/samples/ControlCatalog/Pages/CanvasPage.xaml +++ b/samples/ControlCatalog/Pages/CanvasPage.xaml @@ -1,5 +1,5 @@ - + Canvas A panel which lays out its children by explicit coordinates @@ -31,4 +31,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CarouselPage.xaml b/samples/ControlCatalog/Pages/CarouselPage.xaml index 3468b71fd8..cf9b13c00c 100644 --- a/samples/ControlCatalog/Pages/CarouselPage.xaml +++ b/samples/ControlCatalog/Pages/CarouselPage.xaml @@ -1,9 +1,9 @@ - + Carousel An items control that displays its items as pages that fill the control. - + @@ -20,7 +20,7 @@ - + Transition None @@ -29,7 +29,7 @@ - + Orientation Horizontal @@ -38,4 +38,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/CheckBoxPage.xaml b/samples/ControlCatalog/Pages/CheckBoxPage.xaml index a00b3a7bef..154a6254a4 100644 --- a/samples/ControlCatalog/Pages/CheckBoxPage.xaml +++ b/samples/ControlCatalog/Pages/CheckBoxPage.xaml @@ -1,15 +1,15 @@ - + CheckBox A check box control + Spacing="16"> + Spacing="16"> Unchecked Checked Indeterminate @@ -17,7 +17,7 @@ + Spacing="16"> Three State: Unchecked Three State: Checked Three State: Indeterminate @@ -25,4 +25,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml index 3af823befc..37eeaeb2ac 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml @@ -1,12 +1,12 @@ - + Context Menu A right click menu that can be applied to any control. + Spacing="16"> @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DatePickerPage.xaml b/samples/ControlCatalog/Pages/DatePickerPage.xaml index 92cfa7e178..2c34460fce 100644 --- a/samples/ControlCatalog/Pages/DatePickerPage.xaml +++ b/samples/ControlCatalog/Pages/DatePickerPage.xaml @@ -1,13 +1,13 @@ - + DatePicker A control for selecting dates with a calendar drop-down + Spacing="16"> @@ -43,4 +43,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml b/samples/ControlCatalog/Pages/DialogsPage.xaml index c3e9435630..710d791f3a 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml +++ b/samples/ControlCatalog/Pages/DialogsPage.xaml @@ -1,5 +1,5 @@ - + @@ -9,4 +9,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index af679d2f9a..1f3cd3ff71 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -1,12 +1,12 @@ - + Drag+Drop Example of Drag+Drop capabilities + Spacing="16"> Drag Me @@ -16,4 +16,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/DropDownPage.xaml b/samples/ControlCatalog/Pages/DropDownPage.xaml index 0a7a88e331..5e2a3102e7 100644 --- a/samples/ControlCatalog/Pages/DropDownPage.xaml +++ b/samples/ControlCatalog/Pages/DropDownPage.xaml @@ -1,9 +1,9 @@ - + DropDown A drop-down list. - + Inline Items Inline Item 2 @@ -28,4 +28,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ExpanderPage.xaml b/samples/ControlCatalog/Pages/ExpanderPage.xaml index e32fa1caf1..91440929f5 100644 --- a/samples/ControlCatalog/Pages/ExpanderPage.xaml +++ b/samples/ControlCatalog/Pages/ExpanderPage.xaml @@ -1,12 +1,12 @@ - + Expander Expands to show nested content + Spacing="16"> Expanded content @@ -29,4 +29,4 @@ - \ No newline at end of file + diff --git a/samples/ControlCatalog/Pages/ImagePage.xaml b/samples/ControlCatalog/Pages/ImagePage.xaml index dc93808f27..78fbf90192 100644 --- a/samples/ControlCatalog/Pages/ImagePage.xaml +++ b/samples/ControlCatalog/Pages/ImagePage.xaml @@ -1,12 +1,12 @@ - + Image Displays an image + Spacing="16"> No Stretch - + Menu A window menu + Spacing="16"> diff --git a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml index a5c911f47d..305bcd177c 100644 --- a/samples/ControlCatalog/Pages/NumericUpDownPage.xaml +++ b/samples/ControlCatalog/Pages/NumericUpDownPage.xaml @@ -1,6 +1,6 @@  - + Numeric up-down control Numeric up-down control provides a TextBox with button spinners that allow incrementing and decrementing numeric values by using the spinner buttons, keyboard up/down arrows, or mouse wheel. @@ -26,7 +26,7 @@ VerticalAlignment="Center" Margin="2"> - + @@ -69,7 +69,7 @@ - + Usage of NumericUpDown: - + ProgressBar A progress bar control @@ -7,8 +7,8 @@ - + Spacing="16"> + diff --git a/samples/ControlCatalog/Pages/RadioButtonPage.xaml b/samples/ControlCatalog/Pages/RadioButtonPage.xaml index d382b94f2c..0882817a9a 100644 --- a/samples/ControlCatalog/Pages/RadioButtonPage.xaml +++ b/samples/ControlCatalog/Pages/RadioButtonPage.xaml @@ -1,22 +1,22 @@ - + RadioButton Allows the selection of a single option of many + Spacing="16"> + Spacing="16"> Option 1 Option 2 Option 3 Disabled + Spacing="16"> Three States: Option 1 Three States: Option 2 Three States: Option 3 diff --git a/samples/ControlCatalog/Pages/SliderPage.xaml b/samples/ControlCatalog/Pages/SliderPage.xaml index e43968cb8e..6db71b5fcc 100644 --- a/samples/ControlCatalog/Pages/SliderPage.xaml +++ b/samples/ControlCatalog/Pages/SliderPage.xaml @@ -1,9 +1,9 @@ - + Slider A control that lets the user select from a range of values by moving a Thumb control along a Track. - + - + TextBox A control into which the user can input text - + Spacing="16"> + + - + - + diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml index aa7d60bd11..ad832b9b82 100644 --- a/samples/ControlCatalog/Pages/ToolTipPage.xaml +++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml @@ -1,6 +1,6 @@ + Spacing="4"> ToolTip A control which pops up a hint when a control is hovered diff --git a/samples/ControlCatalog/Pages/TreeViewPage.xaml b/samples/ControlCatalog/Pages/TreeViewPage.xaml index 5806e58c27..1ab49dbb30 100644 --- a/samples/ControlCatalog/Pages/TreeViewPage.xaml +++ b/samples/ControlCatalog/Pages/TreeViewPage.xaml @@ -1,12 +1,12 @@ - + TreeView Displays a hierachical tree of data. + Spacing="16"> diff --git a/samples/VirtualizationDemo/MainWindow.xaml b/samples/VirtualizationDemo/MainWindow.xaml index eb94253d27..730b61ed54 100644 --- a/samples/VirtualizationDemo/MainWindow.xaml +++ b/samples/VirtualizationDemo/MainWindow.xaml @@ -6,7 +6,7 @@ + Spacing="4"> /// Tracks the progress of an animation. /// - public class Animation : AvaloniaList, IDisposable, IAnimation + public class Animation : AvaloniaList, IAnimation { private readonly static List<(Func Condition, Type Animator)> Animators = new List<(Func, Type)> { @@ -24,7 +27,7 @@ namespace Avalonia.Animation }; public static void RegisterAnimator(Func condition) - where TAnimator: IAnimator + where TAnimator : IAnimator { Animators.Insert(0, (condition, typeof(TAnimator))); } @@ -41,8 +44,6 @@ namespace Avalonia.Animation return null; } - private bool _isChildrenChanged = false; - private List _subscription = new List(); public AvaloniaList _animators { get; set; } = new AvaloniaList(); /// @@ -68,22 +69,18 @@ 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. - /// + /// public Easing Easing { get; set; } = new LinearEasing(); - public Animation() - { - this.CollectionChanged += delegate { _isChildrenChanged = true; }; - } - - private void InterpretKeyframes() + private (IList Animators, IList subscriptions) InterpretKeyframes(Animatable control) { - var handlerList = new List<(Type, AvaloniaProperty)>(); - var kfList = new List(); + var handlerList = new List<(Type type, AvaloniaProperty property)>(); + var animatorKeyFrames = new List(); + var subscriptions = new List(); foreach (var keyframe in this) { @@ -99,68 +96,87 @@ 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); + + subscriptions.Add(newKF.BindSetter(setter, control)); + + animatorKeyFrames.Add(newKF); } } - var newAnimatorInstances = new List<(Type handler, AvaloniaProperty prop, IAnimator inst)>(); + var newAnimatorInstances = new List(); - 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, subscriptions); } - /// - /// Cancels the animation. - /// - public void Dispose() + /// + public IDisposable Apply(Animatable control, IObservable match, Action onComplete) { - foreach (var sub in _subscription) + var (animators, subscriptions) = InterpretKeyframes(control); + if (animators.Count == 1) + { + subscriptions.Add(animators[0].Apply(this, control, match, onComplete)); + } + else { - sub.Dispose(); + var completionTasks = onComplete != null ? new List() : null; + foreach (IAnimator animator in animators) + { + Action animatorOnComplete = null; + if (onComplete != null) + { + var tcs = new TaskCompletionSource(); + animatorOnComplete = () => tcs.SetResult(null); + completionTasks.Add(tcs.Task); + } + subscriptions.Add(animator.Apply(this, control, match, animatorOnComplete)); + } + + if (onComplete != null) + { + Task.WhenAll(completionTasks).ContinueWith(_ => onComplete()); + } } + return new CompositeDisposable(subscriptions); } /// - public IDisposable Apply(Animatable control, IObservable matchObs) + public Task RunAsync(Animatable control) { - if (_isChildrenChanged) - { - InterpretKeyframes(); - _isChildrenChanged = false; - } + var run = new TaskCompletionSource(); - foreach (IAnimator keyframes in _animators) + if (this.RepeatCount == RepeatCount.Loop) + run.SetException(new InvalidOperationException("Looping animations must not use the Run method.")); + + IDisposable subscriptions = null; + subscriptions = this.Apply(control, Observable.Return(true), () => { - _subscription.Add(keyframes.Apply(this, control, matchObs)); - } - return this; + run.SetResult(null); + subscriptions?.Dispose(); + }); + + return run.Task; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/AnimatorKeyFrame.cs b/src/Avalonia.Animation/AnimatorKeyFrame.cs index 02457cb9aa..0276c6fa92 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,13 +13,63 @@ namespace Avalonia.Animation /// Defines a KeyFrame that is used for /// objects. /// - 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 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, 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() + { + 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/AnimatorStateMachine`1.cs b/src/Avalonia.Animation/AnimatorStateMachine`1.cs index e37b0e592a..87e189c997 100644 --- a/src/Avalonia.Animation/AnimatorStateMachine`1.cs +++ b/src/Avalonia.Animation/AnimatorStateMachine`1.cs @@ -35,6 +35,7 @@ namespace Avalonia.Animation private T _neutralValue; internal bool _unsubscribe = false; private IObserver _targetObserver; + private readonly Action _onComplete; [Flags] private enum KeyFramesStates @@ -51,9 +52,9 @@ namespace Avalonia.Animation Disposed } - public void Initialize(Animation animation, Animatable control, Animator keyframes) + public AnimatorStateMachine(Animation animation, Animatable control, Animator animator, Action onComplete) { - _parent = keyframes; + _parent = animator; _targetAnimation = animation; _targetControl = control; _neutralValue = (T)_targetControl.GetValue(_parent.Property); @@ -82,6 +83,7 @@ namespace Avalonia.Animation _currentState = KeyFramesStates.DoDelay; else _currentState = KeyFramesStates.DoRun; + _onComplete = onComplete; } public void Step(PlayState _playState, Func Interpolator) @@ -123,121 +125,136 @@ 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; + } + _currentState = KeyFramesStates.Stop; break; - } - _currentState = KeyFramesStates.Stop; - goto checkstate; + case KeyFramesStates.Stop: - case KeyFramesStates.Stop: + if (_fillMode == FillMode.Forward + || _fillMode == FillMode.Both) + { + _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); + } - if (_fillMode == FillMode.Forward - || _fillMode == FillMode.Both) - { - _targetControl.SetValue(_parent.Property, _lastInterpValue, BindingPriority.LocalValue); - } - _targetObserver.OnCompleted(); - break; + _targetObserver.OnCompleted(); + _onComplete?.Invoke(); + Dispose(); + handled = true; + break; + default: + handled = true; + break; + } } } @@ -253,4 +270,4 @@ namespace Avalonia.Animation _currentState = KeyFramesStates.Disposed; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Animation/Animator`1.cs b/src/Avalonia.Animation/Animator`1.cs index 6d4ae7d8e2..eb8b40647d 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 SortedList _convertedKeyframes = new SortedList(); private bool _isVerfifiedAndConverted; @@ -35,18 +35,17 @@ namespace Avalonia.Animation } /// - public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch) + public virtual IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete) { 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); + var timerObs = RunKeyFrames(animation, control, onComplete); }); } @@ -60,8 +59,8 @@ namespace Avalonia.Animation /// The time parameter, relative to the total animation time protected (double IntraKFTime, KeyFramePair KFPair) GetKFPairAndIntraKFTime(double t) { - KeyValuePair firstCue, lastCue; - int kvCount = _convertedKeyframes.Count(); + KeyValuePair 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,26 +88,24 @@ 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)); } /// /// Runs the KeyFrames Animation. /// - private IDisposable RunKeyFrames(Animation animation, Animatable control) + private IDisposable RunKeyFrames(Animation animation, Animatable control, Action onComplete) { - var _kfStateMach = new AnimatorStateMachine(); - _kfStateMach.Initialize(animation, control, this); + var stateMachine = new AnimatorStateMachine(animation, control, this, onComplete); 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); } /// @@ -119,39 +116,19 @@ 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() { - 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 convertedValues) + private void AddNeutralKeyFramesIfNeeded() { bool hasStartKey, hasEndKey; hasStartKey = hasEndKey = false; @@ -170,23 +147,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 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)); } } } -} \ No newline at end of file +} 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/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/IAnimation.cs b/src/Avalonia.Animation/IAnimation.cs index 4de7e46af5..905d90fa52 100644 --- a/src/Avalonia.Animation/IAnimation.cs +++ b/src/Avalonia.Animation/IAnimation.cs @@ -1,6 +1,7 @@ -using System; +using System; using System.Collections.Generic; using System.Text; +using System.Threading.Tasks; namespace Avalonia.Animation { @@ -12,6 +13,11 @@ namespace Avalonia.Animation /// /// Apply the animation to the specified control /// - IDisposable Apply(Animatable control, IObservable match); + IDisposable Apply(Animatable control, IObservable match, Action onComplete = null); + + /// + /// Run the animation to the specified control + /// + Task RunAsync(Animatable control); } } 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/IAnimator.cs b/src/Avalonia.Animation/IAnimator.cs index 6acca4d697..8b763db603 100644 --- a/src/Avalonia.Animation/IAnimator.cs +++ b/src/Avalonia.Animation/IAnimator.cs @@ -17,6 +17,6 @@ namespace Avalonia.Animation /// /// Applies the current KeyFrame group to the specified control. /// - IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch); + IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete); } } 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; } } 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 +} diff --git a/src/Avalonia.Base/Avalonia.Base.csproj b/src/Avalonia.Base/Avalonia.Base.csproj index 26397a6f32..bc068857ff 100644 --- a/src/Avalonia.Base/Avalonia.Base.csproj +++ b/src/Avalonia.Base/Avalonia.Base.csproj @@ -8,4 +8,5 @@ - \ No newline at end of file + + diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index 761c0618da..35e189e6a4 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -22,7 +22,7 @@ namespace Avalonia /// /// This class is analogous to DependencyObject in WPF. /// - public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged, IPriorityValueOwner + public class AvaloniaObject : IAvaloniaObject, IAvaloniaObjectDebug, INotifyPropertyChanged { /// /// The parent object that inherited values are inherited from. @@ -45,21 +45,8 @@ namespace Avalonia /// private EventHandler _propertyChanged; - private DeferredSetter _directDeferredSetter; private ValueStore _values; - - /// - /// Delayed setter helper for direct properties. Used to fix #855. - /// - private DeferredSetter DirectPropertyDeferredSetter - { - get - { - return _directDeferredSetter ?? - (_directDeferredSetter = new DeferredSetter()); - } - } - + private ValueStore Values => _values ?? (_values = new ValueStore(this)); /// /// Initializes a new instance of the 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); } - - /// - 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); } } - - /// - 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. /// /// A collection of property/value tuples. - internal IDictionary GetSetValues() => _values?.GetSetValues(); + internal IDictionary GetSetValues() => Values?.GetSetValues(); /// /// Forces revalidation of properties when a property value changes. @@ -566,12 +546,12 @@ namespace Avalonia T value) { Contract.Requires(setterCallback != null); - return DirectPropertyDeferredSetter.SetAndNotify( + return Values.Setter.SetAndNotify( property, ref field, - (object val, ref T backing, Action notify) => + (object update, ref T backing, 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); } /// diff --git a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs index c0a4ace6ed..e29e7339ae 100644 --- a/src/Avalonia.Base/AvaloniaPropertyRegistry.cs +++ b/src/Avalonia.Base/AvaloniaPropertyRegistry.cs @@ -106,7 +106,7 @@ namespace Avalonia } /// - /// Finds a registered non-attached property on a type by name. + /// Finds a registered property on a type by name. /// /// The type. /// The property name. @@ -130,7 +130,7 @@ namespace Avalonia } /// - /// Finds a registered non-attached property on a type by name. + /// Finds a registered property on an object by name. /// /// The object. /// The property name. @@ -148,52 +148,6 @@ namespace Avalonia return FindRegistered(o.GetType(), name); } - /// - /// Finds a registered attached property on a type by name. - /// - /// The type. - /// The owner type. - /// The property name. - /// - /// The registered property or null if no matching property found. - /// - /// - /// The property name contains a '.'. - /// - public AvaloniaProperty FindRegisteredAttached(Type type, Type ownerType, string name) - { - Contract.Requires(type != null); - Contract.Requires(ownerType != null); - Contract.Requires(name != null); - - if (name.Contains('.')) - { - throw new InvalidOperationException("Attached properties not supported."); - } - - return GetRegisteredAttached(type).FirstOrDefault(x => x.Name == name); - } - - /// - /// Finds a registered non-attached property on a type by name. - /// - /// The object. - /// The owner type. - /// The property name. - /// - /// The registered property or null if no matching property found. - /// - /// - /// The property name contains a '.'. - /// - public AvaloniaProperty FindRegisteredAttached(AvaloniaObject o, Type ownerType, string name) - { - Contract.Requires(o != null); - Contract.Requires(name != null); - - return FindRegisteredAttached(o.GetType(), ownerType, name); - } - /// /// Checks whether a is registered on a type. /// @@ -287,4 +241,4 @@ namespace Avalonia _attachedCache.Clear(); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs new file mode 100644 index 0000000000..fc695762b8 --- /dev/null +++ b/src/Avalonia.Base/Data/Converters/StringFormatValueConverter.cs @@ -0,0 +1,49 @@ +using System; +using System.Globalization; + +namespace Avalonia.Data.Converters +{ + /// + /// A value converter which calls + /// + public class StringFormatValueConverter : IValueConverter + { + /// + /// Initializes a new instance of the class. + /// + /// The format string. + /// + /// An optional inner converter to be called before the format takes place. + /// + public StringFormatValueConverter(string format, IValueConverter inner) + { + Contract.Requires(format != null); + + Format = format; + Inner = inner; + } + + /// + /// Gets an inner value converter which will be called before the string format takes place. + /// + public IValueConverter Inner { get; } + + /// + /// Gets the format string. + /// + public string Format { get; } + + /// + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + { + value = Inner?.Convert(value, targetType, parameter, culture) ?? value; + return string.Format(culture, Format, value); + } + + /// + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + { + throw new NotSupportedException("Two way bindings are not supported with a string format"); + } + } +} diff --git a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs index f44f9043f0..7afbcabd2a 100644 --- a/src/Avalonia.Base/Diagnostics/AvaloniaObjectExtensions.cs +++ b/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 { diff --git a/src/Avalonia.Base/IPriorityValueOwner.cs b/src/Avalonia.Base/IPriorityValueOwner.cs index 5f63f6ef91..8cbf212381 100644 --- a/src/Avalonia.Base/IPriorityValueOwner.cs +++ b/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. /// void VerifyAccess(); + + DeferredSetter Setter { get; } } } diff --git a/src/Avalonia.Base/PriorityValue.cs b/src/Avalonia.Base/PriorityValue.cs index c474f9098e..03094e2236 100644 --- a/src/Avalonia.Base/PriorityValue.cs +++ b/src/Avalonia.Base/PriorityValue.cs @@ -21,7 +21,7 @@ namespace Avalonia /// priority binding that doesn't return . 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 - /// method on the + /// method on the /// owner object is fired with the old and new values. /// internal class PriorityValue @@ -30,7 +30,6 @@ namespace Avalonia private readonly SingleOrDictionary _levels = new SingleOrDictionary(); private readonly Func _validate; - private static readonly DeferredSetter delayedSetter = new DeferredSetter(); private (object value, int priority) _value; /// @@ -243,12 +242,18 @@ namespace Avalonia /// The priority level that the value came from. 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 notify) + => UpdateCore(((object, int))update, ref backing, notify); + private bool UpdateCore( (object value, int priority) update, ref (object value, int priority) backing, diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs similarity index 94% rename from src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs rename to src/Avalonia.Base/Utilities/CharacterReader.cs index dae05eedf9..55be8db043 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Reader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -5,13 +5,13 @@ using System; using System.Globalization; using System.Text; -namespace Avalonia.Markup.Parsers +namespace Avalonia.Utilities { - internal ref struct Reader + public ref struct CharacterReader { private ReadOnlySpan _s; - public Reader(ReadOnlySpan s) + public CharacterReader(ReadOnlySpan s) :this() { _s = s; diff --git a/src/Avalonia.Base/Utilities/DeferredSetter.cs b/src/Avalonia.Base/Utilities/DeferredSetter.cs index fdfa160134..ae6f599005 100644 --- a/src/Avalonia.Base/Utilities/DeferredSetter.cs +++ b/src/Avalonia.Base/Utilities/DeferredSetter.cs @@ -8,11 +8,10 @@ namespace Avalonia.Utilities { /// /// A utility class to enable deferring assignment until after property-changed notifications are sent. + /// Used to fix #855. /// - /// The type of the object that represents the property. /// The type of value with which to track the delayed assignment. - class DeferredSetter - where TProperty: class + class DeferredSetter { private struct NotifyDisposable : IDisposable { @@ -37,29 +36,44 @@ namespace Avalonia.Utilities { public bool Notifying { get; set; } - private Queue pendingValues; + private SingleOrQueue pendingValues; - public Queue PendingValues + public SingleOrQueue PendingValues { get { - return pendingValues ?? (pendingValues = new Queue()); + return pendingValues ?? (pendingValues = new SingleOrQueue()); } } } - private readonly ConditionalWeakTable setRecords = new ConditionalWeakTable(); + private Dictionary _setRecords; + private Dictionary SetRecords + => _setRecords ?? (_setRecords = new Dictionary()); + + private SettingStatus GetOrCreateStatus(AvaloniaProperty property) + { + if (!SetRecords.TryGetValue(property, out var status)) + { + status = new SettingStatus(); + SetRecords.Add(property, status); + } + + return status; + } /// /// Mark the property as currently notifying. /// /// The property to mark as notifying. /// Returns a disposable that when disposed, marks the property as done notifying. - private NotifyDisposable MarkNotifying(TProperty property) + private NotifyDisposable MarkNotifying(AvaloniaProperty property) { Contract.Requires(!IsNotifying(property)); - - return new NotifyDisposable(setRecords.GetOrCreateValue(property)); + + SettingStatus status = GetOrCreateStatus(property); + + return new NotifyDisposable(status); } /// @@ -67,19 +81,19 @@ namespace Avalonia.Utilities /// /// The property. /// If the property is currently notifying listeners. - 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; /// /// Add a pending assignment for the property. /// /// The property. /// The value to assign. - private void AddPendingSet(TProperty property, TSetRecord value) + private void AddPendingSet(AvaloniaProperty property, TSetRecord value) { Contract.Requires(IsNotifying(property)); - setRecords.GetOrCreateValue(property).PendingValues.Enqueue(value); + GetOrCreateStatus(property).PendingValues.Enqueue(value); } /// @@ -87,9 +101,9 @@ namespace Avalonia.Utilities /// /// The property to check. /// If the property has any pending assignments. - 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; } /// @@ -97,9 +111,9 @@ namespace Avalonia.Utilities /// /// The property to check. /// The first pending assignment for the property. - 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(TSetRecord record, ref TValue backing, Action notifyCallback); @@ -115,7 +129,7 @@ namespace Avalonia.Utilities /// /// The value to try to set. public bool SetAndNotify( - TProperty property, + AvaloniaProperty property, ref TValue backing, SetterDelegate setterCallback, TSetRecord value) @@ -144,6 +158,7 @@ namespace Avalonia.Utilities } }); } + return updated; } else if(!object.Equals(value, backing)) diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs b/src/Avalonia.Base/Utilities/IdentifierParser.cs similarity index 93% rename from src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs rename to src/Avalonia.Base/Utilities/IdentifierParser.cs index a2f6c97608..0a2e8e1e1b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/IdentifierParser.cs +++ b/src/Avalonia.Base/Utilities/IdentifierParser.cs @@ -6,11 +6,11 @@ using System.Collections.Generic; using System.Globalization; using System.Text; -namespace Avalonia.Markup.Parsers +namespace Avalonia.Utilities { - internal static class IdentifierParser + public static class IdentifierParser { - public static ReadOnlySpan ParseIdentifier(this ref Reader r) + public static ReadOnlySpan ParseIdentifier(this ref CharacterReader r) { if (IsValidIdentifierStart(r.Peek)) { diff --git a/src/Avalonia.Base/Utilities/SingleOrQueue.cs b/src/Avalonia.Base/Utilities/SingleOrQueue.cs new file mode 100644 index 0000000000..4a66b72a56 --- /dev/null +++ b/src/Avalonia.Base/Utilities/SingleOrQueue.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Utilities +{ + /// + /// FIFO Queue optimized for holding zero or one items. + /// + /// The type of items held in the queue. + public class SingleOrQueue + { + private T _head; + private Queue _tail; + + private Queue Tail => _tail ?? (_tail = new Queue()); + + 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; + } + } +} diff --git a/src/Avalonia.Base/ValueStore.cs b/src/Avalonia.Base/ValueStore.cs index 8283edab80..ab80e74923 100644 --- a/src/Avalonia.Base/ValueStore.cs +++ b/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 GetSetValues() => throw new NotImplementedException(); + public IDictionary 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 _defferedSetter; + + public DeferredSetter Setter + { + get + { + return _defferedSetter ?? + (_defferedSetter = new DeferredSetter()); + } + } } } diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 78dc994df7..13f00bdc87 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -7,11 +7,20 @@ namespace Avalonia.Controls using System; using System.Reactive.Linq; using System.Linq; + using System.ComponentModel; + public class ContextMenu : SelectingItemsControl { private bool _isOpen; private Popup _popup; + /// + /// Defines the property. + /// + public static readonly DirectProperty IsOpenProperty = + AvaloniaProperty.RegisterDirect(nameof(IsOpen), o => o.IsOpen); + + /// /// Initializes static members of the class. /// @@ -22,6 +31,26 @@ namespace Avalonia.Controls MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick, handledEventsToo: true); } + /// + /// Gets a value indicating whether the popup is open + /// + public bool IsOpen => _isOpen; + + /// + /// Occurs when the value of the + /// + /// property is changing from false to true. + /// + public event CancelEventHandler ContextMenuOpening; + + /// + /// Occurs when the value of the + /// + /// property is changing from true to false. + /// + public event CancelEventHandler ContextMenuClosing; + + /// /// Called when the property changes on a control. /// @@ -59,12 +88,12 @@ namespace Avalonia.Controls { if (_popup != null && _popup.IsVisible) { - _popup.Close(); + _popup.IsOpen = false; } SelectedIndex = -1; - _isOpen = false; + SetAndRaise(IsOpenProperty, ref _isOpen, false); } /// @@ -89,11 +118,11 @@ namespace Avalonia.Controls } ((ISetLogicalParent)_popup).SetParent(control); - _popup.Child = control.ContextMenu; + _popup.Child = this; - _popup.Open(); + _popup.IsOpen = true; - control.ContextMenu._isOpen = true; + SetAndRaise(IsOpenProperty, ref _isOpen, true); } } @@ -118,21 +147,37 @@ namespace Avalonia.Controls var control = (Control)sender; var contextMenu = control.ContextMenu; - if (e.MouseButton == MouseButton.Right) + if (control.ContextMenu._isOpen) { - if (control.ContextMenu._isOpen) - { - control.ContextMenu.Hide(); - } + if (contextMenu.CancelClosing()) + return; - contextMenu.Show(control); + control.ContextMenu.Hide(); e.Handled = true; } - else if (contextMenu._isOpen) + + if (e.MouseButton == MouseButton.Right) { - control.ContextMenu.Hide(); + if (contextMenu.CancelOpening()) + return; + + contextMenu.Show(control); e.Handled = true; } } + + private bool CancelClosing() + { + var eventArgs = new CancelEventArgs(); + ContextMenuClosing?.Invoke(this, eventArgs); + return eventArgs.Cancel; + } + + private bool CancelOpening() + { + var eventArgs = new CancelEventArgs(); + ContextMenuOpening?.Invoke(this, eventArgs); + return eventArgs.Cancel; + } } } diff --git a/src/Avalonia.Controls/Expander.cs b/src/Avalonia.Controls/Expander.cs index e7f75336f5..5323939b50 100644 --- a/src/Avalonia.Controls/Expander.cs +++ b/src/Avalonia.Controls/Expander.cs @@ -66,9 +66,7 @@ namespace Avalonia.Controls protected virtual void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e) { - IVisual visualContent = Presenter; - - if (Content != null && ContentTransition != null && visualContent != null) + if (Content != null && ContentTransition != null && Presenter is Visual visualContent) { bool forward = ExpandDirection == ExpandDirection.Left || ExpandDirection == ExpandDirection.Up; @@ -87,4 +85,4 @@ namespace Avalonia.Controls private ExpandDirection _expandDirection; private bool _isExpanded; } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Grid.cs b/src/Avalonia.Controls/Grid.cs index 54fcefeb3f..5f194bdd71 100644 --- a/src/Avalonia.Controls/Grid.cs +++ b/src/Avalonia.Controls/Grid.cs @@ -194,6 +194,16 @@ namespace Avalonia.Controls /// private GridLayout.MeasureResult _rowMeasureCache; + /// + /// Gets the row layout as of the last measure. + /// + private GridLayout _rowLayoutCache; + + /// + /// Gets the column layout as of the last measure. + /// + private GridLayout _columnLayoutCache; + /// /// Measures the grid. /// @@ -253,6 +263,9 @@ namespace Avalonia.Controls // Cache the measure result and return the desired size. _columnMeasureCache = columnResult; _rowMeasureCache = rowResult; + _rowLayoutCache = rowLayout; + _columnLayoutCache = columnLayout; + return new Size(columnResult.DesiredLength, rowResult.DesiredLength); // Measure each child only once. @@ -299,13 +312,11 @@ namespace Avalonia.Controls // arrow back to any statements and re-run them without any side-effect. var (safeColumns, safeRows) = GetSafeColumnRows(); - var columnLayout = new GridLayout(ColumnDefinitions); - var rowLayout = new GridLayout(RowDefinitions); - + var columnLayout = _columnLayoutCache; + var rowLayout = _rowLayoutCache; // Calculate for arrange result. var columnResult = columnLayout.Arrange(finalSize.Width, _columnMeasureCache); var rowResult = rowLayout.Arrange(finalSize.Height, _rowMeasureCache); - // Arrange the children. foreach (var child in Children.OfType()) { @@ -315,7 +326,6 @@ namespace Avalonia.Controls var y = Enumerable.Range(0, row).Sum(r => rowResult.LengthList[r]); var width = Enumerable.Range(column, columnSpan).Sum(c => columnResult.LengthList[c]); var height = Enumerable.Range(row, rowSpan).Sum(r => rowResult.LengthList[r]); - child.Arrange(new Rect(x, y, width, height)); } diff --git a/src/Avalonia.Controls/Image.cs b/src/Avalonia.Controls/Image.cs index f6f11aa9ad..f146e3571c 100644 --- a/src/Avalonia.Controls/Image.cs +++ b/src/Avalonia.Controls/Image.cs @@ -1,12 +1,11 @@ // 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 System; using Avalonia.Media; using Avalonia.Media.Imaging; namespace Avalonia.Controls -{ +{ /// /// Displays a image. /// @@ -68,7 +67,9 @@ namespace Avalonia.Controls Rect sourceRect = new Rect(sourceSize) .CenterRect(new Rect(destRect.Size / scale)); - context.DrawImage(source, 1, sourceRect, destRect); + var interpolationMode = RenderOptions.GetBitmapInterpolationMode(this); + + context.DrawImage(source, 1, sourceRect, destRect, interpolationMode); } } @@ -100,4 +101,4 @@ namespace Avalonia.Controls } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs index fee326dacc..e293cff211 100644 --- a/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs +++ b/src/Avalonia.Controls/Presenters/ItemVirtualizer.cs @@ -161,6 +161,11 @@ namespace Avalonia.Controls.Presenters /// An . public static ItemVirtualizer Create(ItemsPresenter owner) { + if (owner.Panel == null) + { + return null; + } + var virtualizingPanel = owner.Panel as IVirtualizingPanel; var scrollable = (ILogicalScrollable)owner; ItemVirtualizer result = null; diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 590bfa25ac..f8d62a1cbf 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -22,7 +22,6 @@ namespace Avalonia.Controls.Presenters nameof(VirtualizationMode), defaultValue: ItemVirtualizationMode.None); - private ItemVirtualizer _virtualizer; private bool _canHorizontallyScroll; private bool _canVerticallyScroll; @@ -76,21 +75,27 @@ namespace Avalonia.Controls.Presenters /// bool ILogicalScrollable.IsLogicalScrollEnabled { - get { return _virtualizer?.IsLogicalScrollEnabled ?? false; } + get { return Virtualizer?.IsLogicalScrollEnabled ?? false; } } /// - Size IScrollable.Extent => _virtualizer.Extent; + Size IScrollable.Extent => Virtualizer?.Extent ?? Size.Empty; /// Vector IScrollable.Offset { - get { return _virtualizer.Offset; } - set { _virtualizer.Offset = CoerceOffset(value); } + get { return Virtualizer?.Offset ?? new Vector(); } + set + { + if (Virtualizer != null) + { + Virtualizer.Offset = CoerceOffset(value); + } + } } /// - Size IScrollable.Viewport => _virtualizer.Viewport; + Size IScrollable.Viewport => Virtualizer?.Viewport ?? Bounds.Size; /// Action ILogicalScrollable.InvalidateScroll { get; set; } @@ -101,6 +106,8 @@ namespace Avalonia.Controls.Presenters /// Size ILogicalScrollable.PageScrollSize => new Size(0, 1); + internal ItemVirtualizer Virtualizer { get; private set; } + /// bool ILogicalScrollable.BringIntoView(IControl target, Rect targetRect) { @@ -110,29 +117,30 @@ namespace Avalonia.Controls.Presenters /// IControl ILogicalScrollable.GetControlInDirection(NavigationDirection direction, IControl from) { - return _virtualizer?.GetControlInDirection(direction, from); + return Virtualizer?.GetControlInDirection(direction, from); } public override void ScrollIntoView(object item) { - _virtualizer?.ScrollIntoView(item); + Virtualizer?.ScrollIntoView(item); } /// protected override Size MeasureOverride(Size availableSize) { - return _virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; + return Virtualizer?.MeasureOverride(availableSize) ?? Size.Empty; } protected override Size ArrangeOverride(Size finalSize) { - return _virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; + return Virtualizer?.ArrangeOverride(finalSize) ?? Size.Empty; } /// protected override void PanelCreated(IPanel panel) { - _virtualizer = ItemVirtualizer.Create(this); + Virtualizer?.Dispose(); + Virtualizer = ItemVirtualizer.Create(this); ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); if (!Panel.IsSet(KeyboardNavigation.DirectionalNavigationProperty)) @@ -149,7 +157,7 @@ namespace Avalonia.Controls.Presenters protected override void ItemsChanged(NotifyCollectionChangedEventArgs e) { - _virtualizer?.ItemsChanged(Items, e); + Virtualizer?.ItemsChanged(Items, e); } private Vector CoerceOffset(Vector value) @@ -162,8 +170,8 @@ namespace Avalonia.Controls.Presenters private void VirtualizationModeChanged(AvaloniaPropertyChangedEventArgs e) { - _virtualizer?.Dispose(); - _virtualizer = ItemVirtualizer.Create(this); + Virtualizer?.Dispose(); + Virtualizer = ItemVirtualizer.Create(this); ((ILogicalScrollable)this).InvalidateScroll?.Invoke(); } } diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs index 5a56e52029..e9dc75a236 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs @@ -4,6 +4,7 @@ using System; using System.Collections; using System.Collections.Specialized; +using Avalonia.Collections; using Avalonia.Controls.Generators; using Avalonia.Controls.Templates; using Avalonia.Styling; @@ -40,6 +41,7 @@ namespace Avalonia.Controls.Presenters ItemsControl.MemberSelectorProperty.AddOwner(); private IEnumerable _items; + private IDisposable _itemsSubscription; private bool _createdPanel; private IItemContainerGenerator _generator; @@ -63,24 +65,12 @@ namespace Avalonia.Controls.Presenters set { - if (_createdPanel) - { - INotifyCollectionChanged incc = _items as INotifyCollectionChanged; - - if (incc != null) - { - incc.CollectionChanged -= ItemsCollectionChanged; - } - } + _itemsSubscription?.Dispose(); + _itemsSubscription = null; - if (_createdPanel && value != null) + if (_createdPanel && value is INotifyCollectionChanged incc) { - INotifyCollectionChanged incc = value as INotifyCollectionChanged; - - if (incc != null) - { - incc.CollectionChanged += ItemsCollectionChanged; - } + _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } SetAndRaise(ItemsProperty, ref _items, value); @@ -233,11 +223,9 @@ namespace Avalonia.Controls.Presenters _createdPanel = true; - INotifyCollectionChanged incc = Items as INotifyCollectionChanged; - - if (incc != null) + if (_itemsSubscription == null && Items is INotifyCollectionChanged incc) { - incc.CollectionChanged += ItemsCollectionChanged; + _itemsSubscription = incc.WeakSubscribe(ItemsCollectionChanged); } PanelCreated(Panel); @@ -263,4 +251,4 @@ namespace Avalonia.Controls.Presenters (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index bda660be51..636c836da5 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -189,6 +189,12 @@ namespace Avalonia.Controls.Presenters _caretTimer.Start(); InvalidateVisual(); } + else + { + _caretTimer.Start(); + InvalidateVisual(); + _caretTimer.Stop(); + } if (IsMeasureValid) { diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 457a7bd4b4..0ae4be5550 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -83,21 +83,22 @@ namespace Avalonia.Controls.Primitives /// public void SnapInsideScreenEdges() { - var window = this.GetSelfAndLogicalAncestors().OfType().First(); - - var screen = window.Screens.ScreenFromPoint(Position); + var screen = Application.Current.MainWindow?.Screens.ScreenFromPoint(Position); - var screenX = Position.X + Bounds.Width - screen.Bounds.X; - var screenY = Position.Y + Bounds.Height - screen.Bounds.Y; - - if (screenX > screen.Bounds.Width) + if (screen != null) { - Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); - } + var screenX = Position.X + Bounds.Width - screen.Bounds.X; + var screenY = Position.Y + Bounds.Height - screen.Bounds.Y; - if (screenY > screen.Bounds.Height) - { - Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); + if (screenX > screen.Bounds.Width) + { + Position = Position.WithX(Position.X - (screenX - screen.Bounds.Width)); + } + + if (screenY > screen.Bounds.Height) + { + Position = Position.WithY(Position.Y - (screenY - screen.Bounds.Height)); + } } } diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index a7b8981583..c8425a0f80 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -360,7 +360,7 @@ namespace Avalonia.Controls.Primitives { if (!AlwaysSelected) { - SelectedIndex = -1; + selectedIndex = SelectedIndex = -1; } else { @@ -368,10 +368,15 @@ namespace Avalonia.Controls.Primitives } } + var items = Items?.Cast(); + if (selectedIndex >= items.Count()) + { + selectedIndex = SelectedIndex = items.Count() - 1; + } break; case NotifyCollectionChangedAction.Reset: - SelectedIndex = IndexOf(e.NewItems, SelectedItem); + SelectedIndex = IndexOf(Items, SelectedItem); break; } } diff --git a/src/Avalonia.Controls/Primitives/ToggleButton.cs b/src/Avalonia.Controls/Primitives/ToggleButton.cs index dc9b70ab8c..1d2e2f2100 100644 --- a/src/Avalonia.Controls/Primitives/ToggleButton.cs +++ b/src/Avalonia.Controls/Primitives/ToggleButton.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls.Primitives nameof(IsChecked), o => o.IsChecked, (o, v) => o.IsChecked = v, - unsetValue: false, + unsetValue: null, defaultBindingMode: BindingMode.TwoWay); public static readonly StyledProperty IsThreeStateProperty = diff --git a/src/Avalonia.Controls/ProgressBar.cs b/src/Avalonia.Controls/ProgressBar.cs index 5e5a460368..b7db352c74 100644 --- a/src/Avalonia.Controls/ProgressBar.cs +++ b/src/Avalonia.Controls/ProgressBar.cs @@ -21,18 +21,27 @@ namespace Avalonia.Controls public static readonly StyledProperty OrientationProperty = AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + private static readonly DirectProperty IndeterminateStartingOffsetProperty = + AvaloniaProperty.RegisterDirect( + nameof(IndeterminateStartingOffset), + p => p.IndeterminateStartingOffset, + (p, o) => p.IndeterminateStartingOffset = o); + + private static readonly DirectProperty IndeterminateEndingOffsetProperty = + AvaloniaProperty.RegisterDirect( + 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(x => x.ValueChanged); - - IsIndeterminateProperty.Changed.AddClassHandler( - (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); + } /// protected override Size ArrangeOverride(Size finalSize) @@ -60,7 +82,6 @@ namespace Avalonia.Controls _indicator = e.NameScope.Get("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; - - 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.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 2ef0af2852..31113812d1 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -17,7 +17,7 @@ namespace Avalonia.Controls /// Defines the property. /// public static readonly StyledProperty OrientationProperty = - AvaloniaProperty.Register(nameof(Orientation), Orientation.Horizontal); + ScrollBar.OrientationProperty.AddOwner(); /// /// Defines the property. @@ -41,8 +41,7 @@ namespace Avalonia.Controls /// static Slider() { - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Vertical, ":vertical"); - PseudoClass(OrientationProperty, o => o == Avalonia.Controls.Orientation.Horizontal, ":horizontal"); + OrientationProperty.OverrideDefaultValue(typeof(Slider), Orientation.Horizontal); Thumb.DragStartedEvent.AddClassHandler(x => x.OnThumbDragStarted, RoutingStrategies.Bubble); Thumb.DragDeltaEvent.AddClassHandler(x => x.OnThumbDragDelta, RoutingStrategies.Bubble); Thumb.DragCompletedEvent.AddClassHandler(x => x.OnThumbDragCompleted, RoutingStrategies.Bubble); diff --git a/src/Avalonia.Controls/StackPanel.cs b/src/Avalonia.Controls/StackPanel.cs index a6fe35d668..b0ccd8a3d1 100644 --- a/src/Avalonia.Controls/StackPanel.cs +++ b/src/Avalonia.Controls/StackPanel.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Linq; using Avalonia.Input; namespace Avalonia.Controls @@ -12,10 +13,10 @@ namespace Avalonia.Controls public class StackPanel : Panel, INavigableContainer { /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty GapProperty = - AvaloniaProperty.Register(nameof(Gap)); + public static readonly StyledProperty SpacingProperty = + AvaloniaProperty.Register(nameof(Spacing)); /// /// Defines the property. @@ -28,17 +29,17 @@ namespace Avalonia.Controls /// static StackPanel() { - AffectsMeasure(GapProperty); + AffectsMeasure(SpacingProperty); AffectsMeasure(OrientationProperty); } /// - /// Gets or sets the size of the gap to place between child controls. + /// Gets or sets the size of the spacing to place between child controls. /// - public double Gap + public double Spacing { - get { return GetValue(GapProperty); } - set { SetValue(GapProperty, value); } + get { return GetValue(SpacingProperty); } + set { SetValue(SpacingProperty, value); } } /// @@ -151,7 +152,8 @@ namespace Avalonia.Controls double measuredWidth = 0; double measuredHeight = 0; - double gap = Gap; + double spacing = Spacing; + bool hasVisibleChild = Children.Any(c => c.IsVisible); foreach (Control child in Children) { @@ -160,23 +162,23 @@ namespace Avalonia.Controls if (Orientation == Orientation.Vertical) { - measuredHeight += size.Height + gap; + measuredHeight += size.Height + (child.IsVisible ? spacing : 0); measuredWidth = Math.Max(measuredWidth, size.Width); } else { - measuredWidth += size.Width + gap; + measuredWidth += size.Width + (child.IsVisible ? spacing : 0); measuredHeight = Math.Max(measuredHeight, size.Height); } } if (Orientation == Orientation.Vertical) { - measuredHeight -= gap; + measuredHeight -= (hasVisibleChild ? spacing : 0); } else { - measuredWidth -= gap; + measuredWidth -= (hasVisibleChild ? spacing : 0); } return new Size(measuredWidth, measuredHeight); @@ -192,7 +194,8 @@ namespace Avalonia.Controls var orientation = Orientation; double arrangedWidth = finalSize.Width; double arrangedHeight = finalSize.Height; - double gap = Gap; + double spacing = Spacing; + bool hasVisibleChild = Children.Any(c => c.IsVisible); if (Orientation == Orientation.Vertical) { @@ -214,25 +217,25 @@ namespace Avalonia.Controls Rect childFinal = new Rect(0, arrangedHeight, width, childHeight); ArrangeChild(child, childFinal, finalSize, orientation); arrangedWidth = Math.Max(arrangedWidth, childWidth); - arrangedHeight += childHeight + gap; + arrangedHeight += childHeight + (child.IsVisible ? spacing : 0); } else { double height = Math.Max(childHeight, arrangedHeight); Rect childFinal = new Rect(arrangedWidth, 0, childWidth, height); ArrangeChild(child, childFinal, finalSize, orientation); - arrangedWidth += childWidth + gap; + arrangedWidth += childWidth + (child.IsVisible ? spacing : 0); arrangedHeight = Math.Max(arrangedHeight, childHeight); } } if (orientation == Orientation.Vertical) { - arrangedHeight = Math.Max(arrangedHeight - gap, finalSize.Height); + arrangedHeight = Math.Max(arrangedHeight - (hasVisibleChild ? spacing : 0), finalSize.Height); } else { - arrangedWidth = Math.Max(arrangedWidth - gap, finalSize.Width); + arrangedWidth = Math.Max(arrangedWidth - (hasVisibleChild ? spacing : 0), finalSize.Width); } return new Size(arrangedWidth, arrangedHeight); @@ -247,4 +250,4 @@ namespace Avalonia.Controls child.Arrange(rect); } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 2ea9319194..388f984b78 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -124,7 +124,7 @@ namespace Avalonia.Controls ScrollViewer.HorizontalScrollBarVisibilityProperty, horizontalScrollBarVisibility, BindingPriority.Style); - _undoRedoHelper = new UndoRedoHelper(this); + _undoRedoHelper = new UndoRedoHelper(this); } public bool AcceptsReturn @@ -262,7 +262,7 @@ namespace Avalonia.Controls if (IsFocused) { - _presenter.ShowCaret(); + DecideCaretVisibility(); } } @@ -282,12 +282,20 @@ namespace Avalonia.Controls } else { - _presenter?.ShowCaret(); + DecideCaretVisibility(); } e.Handled = true; } + private void DecideCaretVisibility() + { + if (!IsReadOnly) + _presenter?.ShowCaret(); + else + _presenter?.HideCaret(); + } + protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); @@ -557,7 +565,7 @@ namespace Avalonia.Controls var index = CaretIndex = _presenter.GetCaretIndex(point); var text = Text; - if (text != null) + if (text != null && e.MouseButton == MouseButton.Left) { switch (e.ClickCount) { diff --git a/src/Avalonia.Controls/VirtualizingStackPanel.cs b/src/Avalonia.Controls/VirtualizingStackPanel.cs index dee537029c..5f7b63c57a 100644 --- a/src/Avalonia.Controls/VirtualizingStackPanel.cs +++ b/src/Avalonia.Controls/VirtualizingStackPanel.cs @@ -200,7 +200,7 @@ namespace Avalonia.Controls private void UpdateAdd(IControl child) { var bounds = Bounds; - var gap = Gap; + var spacing = Spacing; child.Measure(_availableSpace); ++_averageCount; @@ -208,13 +208,13 @@ namespace Avalonia.Controls if (Orientation == Orientation.Vertical) { var height = child.DesiredSize.Height; - _takenSpace += height + gap; + _takenSpace += height + spacing; AddToAverageItemSize(height); } else { var width = child.DesiredSize.Width; - _takenSpace += width + gap; + _takenSpace += width + spacing; AddToAverageItemSize(width); } } @@ -222,18 +222,18 @@ namespace Avalonia.Controls private void UpdateRemove(IControl child) { var bounds = Bounds; - var gap = Gap; + var spacing = Spacing; if (Orientation == Orientation.Vertical) { var height = child.DesiredSize.Height; - _takenSpace -= height + gap; + _takenSpace -= height + spacing; RemoveFromAverageItemSize(height); } else { var width = child.DesiredSize.Width; - _takenSpace -= width + gap; + _takenSpace -= width + spacing; RemoveFromAverageItemSize(width); } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index d1a023c42c..7e1d8f18f0 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -302,17 +302,23 @@ namespace Avalonia.Controls internal void Close(bool ignoreCancel) { + bool close = true; + try { if (!ignoreCancel && HandleClosing()) { + close = false; return; } } finally { - PlatformImpl?.Dispose(); - HandleClosed(); + if (close) + { + PlatformImpl?.Dispose(); + HandleClosed(); + } } } diff --git a/src/Avalonia.Controls/WindowCollection.cs b/src/Avalonia.Controls/WindowCollection.cs index c21a12f05b..df79c3e3c8 100644 --- a/src/Avalonia.Controls/WindowCollection.cs +++ b/src/Avalonia.Controls/WindowCollection.cs @@ -96,7 +96,7 @@ namespace Avalonia { while (_windows.Count > 0) { - _windows[0].Close(); + _windows[0].Close(true); } } @@ -131,4 +131,4 @@ namespace Avalonia } } } -} \ No newline at end of file +} diff --git a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj index 9f54137e47..5ccb98b64d 100644 --- a/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj +++ b/src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj @@ -1,7 +1,12 @@  netstandard2.0 - false + + 0.7.0 true @@ -37,11 +42,6 @@ - - - Properties\SharedAssemblyInfo.cs - - \ No newline at end of file diff --git a/src/Avalonia.Diagnostics/DevTools.xaml b/src/Avalonia.Diagnostics/DevTools.xaml index bb1b2e841e..844670e794 100644 --- a/src/Avalonia.Diagnostics/DevTools.xaml +++ b/src/Avalonia.Diagnostics/DevTools.xaml @@ -7,7 +7,7 @@ - + Hold Ctrl+Shift over a control to inspect. Focused: @@ -17,4 +17,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs b/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs index 555a0b2354..ce8ad36c17 100644 --- a/src/Avalonia.Diagnostics/ViewModels/DevToolsViewModel.cs +++ b/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); } diff --git a/src/Avalonia.Diagnostics/Views/TreePageView.xaml b/src/Avalonia.Diagnostics/Views/TreePageView.xaml index a715ca6fc5..57398851ad 100644 --- a/src/Avalonia.Diagnostics/Views/TreePageView.xaml +++ b/src/Avalonia.Diagnostics/Views/TreePageView.xaml @@ -5,7 +5,7 @@ - + @@ -21,4 +21,4 @@ - \ No newline at end of file + diff --git a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj index f80462e958..0aed0a9717 100644 --- a/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj +++ b/src/Avalonia.DotNetCoreRuntime/Avalonia.DotNetCoreRuntime.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Avalonia.Styling/Styling/Setter.cs b/src/Avalonia.Styling/Styling/Setter.cs index 1a78e0f4d7..c75bae4db8 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) @@ -158,18 +158,11 @@ namespace Avalonia.Styling var activated = new ActivatedObservable(activator, sourceInstance.Observable, description); return InstancedBinding.OneWay(activated, BindingPriority.StyleTrigger); } - case BindingMode.OneWayToSource: - { - var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return InstancedBinding.OneWayToSource(activated, BindingPriority.StyleTrigger); - } - case BindingMode.TwoWay: + default: { var activated = new ActivatedSubject(activator, sourceInstance.Subject, description); - return InstancedBinding.TwoWay(activated, BindingPriority.StyleTrigger); + return new InstancedBinding(activated, sourceInstance.Mode, BindingPriority.StyleTrigger); } - default: - throw new NotSupportedException("Unsupported BindingMode."); } } diff --git a/src/Avalonia.Themes.Default/MenuItem.xaml b/src/Avalonia.Themes.Default/MenuItem.xaml index 47b4528f88..53965db016 100644 --- a/src/Avalonia.Themes.Default/MenuItem.xaml +++ b/src/Avalonia.Themes.Default/MenuItem.xaml @@ -122,6 +122,11 @@ + + - \ No newline at end of file + + + diff --git a/src/Avalonia.Visuals/Animation/CrossFade.cs b/src/Avalonia.Visuals/Animation/CrossFade.cs index 410ad0a3b3..2230f8534b 100644 --- a/src/Avalonia.Visuals/Animation/CrossFade.cs +++ b/src/Avalonia.Visuals/Animation/CrossFade.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Reactive.Threading.Tasks; using System.Threading.Tasks; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Animation @@ -14,10 +15,14 @@ namespace Avalonia.Animation /// public class CrossFade : IPageTransition { + private Animation _fadeOutAnimation; + private Animation _fadeInAnimation; + /// /// Initializes a new instance of the class. /// public CrossFade() + :this(TimeSpan.Zero) { } @@ -27,13 +32,51 @@ namespace Avalonia.Animation /// The duration of the animation. public CrossFade(TimeSpan duration) { - Duration = duration; + _fadeOutAnimation = new Animation + { + new KeyFrame + ( + new Setter + { + Property = Visual.OpacityProperty, + Value = 0.0 + } + ) + { + Cue = new Cue(1.0) + } + }; + _fadeInAnimation = new Animation + { + new KeyFrame + ( + new Setter + { + Property = Visual.OpacityProperty, + Value = 0.0 + } + ) + { + Cue = new Cue(0.0) + } + }; + _fadeOutAnimation.Duration = _fadeInAnimation.Duration = duration; } /// /// Gets the duration of the animation. /// - public TimeSpan Duration { get; set; } + public TimeSpan Duration + { + get + { + return _fadeOutAnimation.Duration; + } + set + { + _fadeOutAnimation.Duration = _fadeInAnimation.Duration = value; + } + } /// /// Starts the animation. @@ -47,12 +90,10 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - public async Task Start(IVisual from, IVisual to) + public async Task Start(Visual from, Visual to) { var tasks = new List(); - // TODO: Implement relevant transition logic here (or discard this class) - // in favor of XAML based transition for pages if (to != null) { to.Opacity = 0; @@ -60,22 +101,21 @@ namespace Avalonia.Animation if (from != null) { + tasks.Add(_fadeOutAnimation.RunAsync(from)); } if (to != null) { - to.Opacity = 0; to.IsVisible = true; + tasks.Add(_fadeInAnimation.RunAsync(to)); } - // FIXME: This is temporary until animations are fixed. - await Task.Delay(1); + await Task.WhenAll(tasks); if (from != null) { from.IsVisible = false; - from.Opacity = 1; } if (to != null) @@ -99,7 +139,7 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - Task IPageTransition.Start(IVisual from, IVisual to, bool forward) + Task IPageTransition.Start(Visual from, Visual to, bool forward) { return Start(from, to); } diff --git a/src/Avalonia.Visuals/Animation/IPageTransition.cs b/src/Avalonia.Visuals/Animation/IPageTransition.cs index 6dc64de049..88912c1931 100644 --- a/src/Avalonia.Visuals/Animation/IPageTransition.cs +++ b/src/Avalonia.Visuals/Animation/IPageTransition.cs @@ -26,6 +26,6 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - Task Start(IVisual from, IVisual to, bool forward); + Task Start(Visual from, Visual to, bool forward); } } diff --git a/src/Avalonia.Visuals/Animation/PageSlide.cs b/src/Avalonia.Visuals/Animation/PageSlide.cs index 13f9e67d29..5f03dc6b0c 100644 --- a/src/Avalonia.Visuals/Animation/PageSlide.cs +++ b/src/Avalonia.Visuals/Animation/PageSlide.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Reactive.Threading.Tasks; using System.Threading.Tasks; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Animation @@ -67,7 +68,7 @@ namespace Avalonia.Animation /// /// A that tracks the progress of the animation. /// - public async Task Start(IVisual from, IVisual to, bool forward) + public async Task Start(Visual from, Visual to, bool forward) { var tasks = new List(); var parent = GetVisualParent(from, to); @@ -79,16 +80,69 @@ namespace Avalonia.Animation // in favor of XAML based transition for pages if (from != null) { - + var animation = new Animation + { + new KeyFrame + ( + new Setter + { + Property = translateProperty, + Value = 0 + } + ) + { + Cue = new Cue(0.0) + }, + new KeyFrame + ( + new Setter + { + Property = translateProperty, + Value = forward ? -distance : distance + } + ) + { + Cue = new Cue(1.0) + } + }; + animation.Duration = Duration; + tasks.Add(animation.RunAsync(from)); } if (to != null) { - + to.IsVisible = true; + var animation = new Animation + { + + new KeyFrame + ( + new Setter + { + Property = translateProperty, + Value = forward ? -distance : distance + } + ) + { + Cue = new Cue(0.0) + }, + new KeyFrame + ( + new Setter + { + Property = translateProperty, + Value = 0 + } + ) + { + Cue = new Cue(1.0) + }, + }; + animation.Duration = Duration; + tasks.Add(animation.RunAsync(to)); } - // FIXME: This is temporary until animations are fixed. - await Task.Delay(1); + await Task.WhenAll(tasks); if (from != null) { diff --git a/src/Avalonia.Visuals/Animation/TransformAnimator.cs b/src/Avalonia.Visuals/Animation/TransformAnimator.cs index 46cefbd061..61cac695b1 100644 --- a/src/Avalonia.Visuals/Animation/TransformAnimator.cs +++ b/src/Avalonia.Visuals/Animation/TransformAnimator.cs @@ -19,7 +19,7 @@ namespace Avalonia.Animation DoubleAnimator childKeyFrames; /// - public override IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch) + public override IDisposable Apply(Animation animation, Animatable control, IObservable obsMatch, Action onComplete) { var ctrl = (Visual)control; @@ -51,7 +51,7 @@ namespace Avalonia.Animation // It's a transform object so let's target that. if (renderTransformType == Property.OwnerType) { - return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch); + return childKeyFrames.Apply(animation, ctrl.RenderTransform, obsMatch, onComplete); } // It's a TransformGroup and try finding the target there. else if (renderTransformType == typeof(TransformGroup)) @@ -60,7 +60,7 @@ namespace Avalonia.Animation { if (transform.GetType() == Property.OwnerType) { - return childKeyFrames.Apply(animation, transform, obsMatch); + return childKeyFrames.Apply(animation, transform, obsMatch, onComplete); } } } diff --git a/src/Avalonia.Visuals/Media/DrawingContext.cs b/src/Avalonia.Visuals/Media/DrawingContext.cs index 962f2c1ba8..60a7a2e518 100644 --- a/src/Avalonia.Visuals/Media/DrawingContext.cs +++ b/src/Avalonia.Visuals/Media/DrawingContext.cs @@ -2,9 +2,10 @@ using System; using System.Collections.Generic; using Avalonia.Media.Imaging; using Avalonia.Platform; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media -{ +{ public sealed class DrawingContext : IDisposable { private int _currentLevel; @@ -68,11 +69,12 @@ namespace Avalonia.Media /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect) + /// The bitmap interpolation mode. + public void DrawImage(IBitmap source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = default) { Contract.Requires(source != null); - PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect); + PlatformImpl.DrawImage(source.PlatformImpl, opacity, sourceRect, destRect, bitmapInterpolationMode); } /// @@ -309,4 +311,4 @@ namespace Avalonia.Media return pen?.Brush != null && pen.Thickness > 0; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/ITileBrush.cs b/src/Avalonia.Visuals/Media/ITileBrush.cs index 8e2349f506..5cffe02193 100644 --- a/src/Avalonia.Visuals/Media/ITileBrush.cs +++ b/src/Avalonia.Visuals/Media/ITileBrush.cs @@ -1,5 +1,10 @@ -namespace Avalonia.Media -{ +// 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.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ /// /// A brush which displays a repeating image. /// @@ -35,5 +40,13 @@ /// Gets the brush's tile mode. /// TileMode TileMode { get; } + + /// + /// Gets the bitmap interpolation mode. + /// + /// + /// The bitmap interpolation mode. + /// + BitmapInterpolationMode BitmapInterpolationMode { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs b/src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs new file mode 100644 index 0000000000..7e6d330618 --- /dev/null +++ b/src/Avalonia.Visuals/Media/Imaging/BitmapInterpolationMode.cs @@ -0,0 +1,31 @@ +// 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. + +namespace Avalonia.Visuals.Media.Imaging +{ + /// + /// Controls the performance and quality of bitmap scaling. + /// + public enum BitmapInterpolationMode + { + /// + /// Uses the default behavior of the underling render backend. + /// + Default, + + /// + /// The best performance but worst image quality. + /// + LowQuality, + + /// + /// Good performance and decent image quality. + /// + MediumQuality, + + /// + /// Highest quality but worst performance. + /// + HighQuality + } +} diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs index 678f99c20d..15da8f8b43 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableImageBrush.cs @@ -1,5 +1,5 @@ -using System; -using Avalonia.Media.Imaging; +using Avalonia.Media.Imaging; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Media.Immutable { @@ -21,6 +21,7 @@ namespace Avalonia.Media.Immutable /// How the source rectangle will be stretched to fill the destination rect. /// /// The tile mode. + /// The bitmap interpolation mode. public ImmutableImageBrush( IBitmap source, AlignmentX alignmentX = AlignmentX.Center, @@ -29,7 +30,8 @@ namespace Avalonia.Media.Immutable double opacity = 1, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, - TileMode tileMode = TileMode.None) + TileMode tileMode = TileMode.None, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) : base( alignmentX, alignmentY, @@ -37,7 +39,8 @@ namespace Avalonia.Media.Immutable opacity, sourceRect ?? RelativeRect.Fill, stretch, - tileMode) + tileMode, + bitmapInterpolationMode) { Source = source; } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs index 37c391040f..c3dd159d04 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableTileBrush.cs @@ -1,4 +1,7 @@ -using System; +// 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.Visuals.Media.Imaging; namespace Avalonia.Media.Immutable { @@ -19,6 +22,7 @@ namespace Avalonia.Media.Immutable /// How the source rectangle will be stretched to fill the destination rect. /// /// The tile mode. + /// The bitmap interpolation mode. protected ImmutableTileBrush( AlignmentX alignmentX, AlignmentY alignmentY, @@ -26,7 +30,8 @@ namespace Avalonia.Media.Immutable double opacity, RelativeRect sourceRect, Stretch stretch, - TileMode tileMode) + TileMode tileMode, + BitmapInterpolationMode bitmapInterpolationMode) { AlignmentX = alignmentX; AlignmentY = alignmentY; @@ -35,6 +40,7 @@ namespace Avalonia.Media.Immutable SourceRect = sourceRect; Stretch = stretch; TileMode = tileMode; + BitmapInterpolationMode = bitmapInterpolationMode; } /// @@ -49,7 +55,8 @@ namespace Avalonia.Media.Immutable source.Opacity, source.SourceRect, source.Stretch, - source.TileMode) + source.TileMode, + source.BitmapInterpolationMode) { } @@ -73,5 +80,8 @@ namespace Avalonia.Media.Immutable /// public TileMode TileMode { get; } + + /// + public BitmapInterpolationMode BitmapInterpolationMode { get; } } } diff --git a/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs b/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs index b07d98867c..f1f61a6e65 100644 --- a/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs +++ b/src/Avalonia.Visuals/Media/Immutable/ImmutableVisualBrush.cs @@ -1,4 +1,7 @@ -using System; +// 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.Visuals.Media.Imaging; using Avalonia.VisualTree; namespace Avalonia.Media.Immutable @@ -21,6 +24,7 @@ namespace Avalonia.Media.Immutable /// How the source rectangle will be stretched to fill the destination rect. /// /// The tile mode. + /// Controls the quality of interpolation. public ImmutableVisualBrush( IVisual visual, AlignmentX alignmentX = AlignmentX.Center, @@ -29,7 +33,8 @@ namespace Avalonia.Media.Immutable double opacity = 1, RelativeRect? sourceRect = null, Stretch stretch = Stretch.Uniform, - TileMode tileMode = TileMode.None) + TileMode tileMode = TileMode.None, + BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default) : base( alignmentX, alignmentY, @@ -37,7 +42,8 @@ namespace Avalonia.Media.Immutable opacity, sourceRect ?? RelativeRect.Fill, stretch, - tileMode) + tileMode, + bitmapInterpolationMode) { Visual = visual; } diff --git a/src/Avalonia.Visuals/Media/RenderOptions.cs b/src/Avalonia.Visuals/Media/RenderOptions.cs new file mode 100644 index 0000000000..180863f9e8 --- /dev/null +++ b/src/Avalonia.Visuals/Media/RenderOptions.cs @@ -0,0 +1,39 @@ +// 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.Visuals.Media.Imaging; + +namespace Avalonia.Media +{ + public class RenderOptions + { + /// + /// Defines the property. + /// + public static readonly StyledProperty BitmapInterpolationModeProperty = + AvaloniaProperty.RegisterAttached( + "BitmapInterpolationMode", + BitmapInterpolationMode.MediumQuality, + inherits: true); + + /// + /// Gets the value of the BitmapInterpolationMode attached property for a control. + /// + /// The control. + /// The control's left coordinate. + public static BitmapInterpolationMode GetBitmapInterpolationMode(AvaloniaObject element) + { + return element.GetValue(BitmapInterpolationModeProperty); + } + + /// + /// Sets the value of the BitmapInterpolationMode attached property for a control. + /// + /// The control. + /// The left value. + public static void SetBitmapInterpolationMode(AvaloniaObject element, BitmapInterpolationMode value) + { + element.SetValue(BitmapInterpolationModeProperty, value); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TileBrush.cs b/src/Avalonia.Visuals/Media/TileBrush.cs index 3a7f9d9920..2033754137 100644 --- a/src/Avalonia.Visuals/Media/TileBrush.cs +++ b/src/Avalonia.Visuals/Media/TileBrush.cs @@ -1,6 +1,8 @@ // 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.Visuals.Media.Imaging; + namespace Avalonia.Media { /// @@ -75,6 +77,11 @@ namespace Avalonia.Media public static readonly StyledProperty TileModeProperty = AvaloniaProperty.Register(nameof(TileMode)); + static TileBrush() + { + RenderOptions.BitmapInterpolationModeProperty.OverrideDefaultValue(BitmapInterpolationMode.Default); + } + /// /// Gets or sets the horizontal alignment of a tile in the destination. /// @@ -129,5 +136,17 @@ namespace Avalonia.Media get { return (TileMode)GetValue(TileModeProperty); } set { SetValue(TileModeProperty, value); } } + + /// + /// Gets or sets the bitmap interpolation mode. + /// + /// + /// The bitmap interpolation mode. + /// + public BitmapInterpolationMode BitmapInterpolationMode + { + get { return RenderOptions.GetBitmapInterpolationMode(this); } + set { RenderOptions.SetBitmapInterpolationMode(this, value); } + } } } diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs index 04bbe713f2..b11d9f52ab 100644 --- a/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Platform/IDrawingContextImpl.cs @@ -4,6 +4,7 @@ using System; using Avalonia.Media; using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Platform { @@ -30,7 +31,8 @@ namespace Avalonia.Platform /// The opacity to draw with. /// The rect in the image to draw. /// The rect in the output to draw to. - void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect); + /// The bitmap interpolation mode. + void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode = BitmapInterpolationMode.Default); /// /// Draws a bitmap image. diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index f3dbfbd8fb..42ecde6a6d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -7,6 +7,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; using Avalonia.VisualTree; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Rendering.SceneGraph { @@ -114,13 +115,13 @@ namespace Avalonia.Rendering.SceneGraph } /// - public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect) + public void DrawImage(IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { var next = NextDrawAs(); - if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect)) + if (next == null || !next.Item.Equals(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)) { - Add(new ImageNode(Transform, source, opacity, sourceRect, destRect)); + Add(new ImageNode(Transform, source, opacity, sourceRect, destRect, bitmapInterpolationMode)); } else { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs index 06fdb3f86c..9e8fca5f84 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ImageNode.cs @@ -4,6 +4,7 @@ using System; using Avalonia.Platform; using Avalonia.Utilities; +using Avalonia.Visuals.Media.Imaging; namespace Avalonia.Rendering.SceneGraph { @@ -20,7 +21,8 @@ namespace Avalonia.Rendering.SceneGraph /// The draw opacity. /// The source rect. /// The destination rect. - public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect) + /// The bitmap interpolation mode. + public ImageNode(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) : base(destRect, transform, null) { Transform = transform; @@ -28,7 +30,8 @@ namespace Avalonia.Rendering.SceneGraph Opacity = opacity; SourceRect = sourceRect; DestRect = destRect; - } + BitmapInterpolationMode = bitmapInterpolationMode; + } /// /// Gets the transform with which the node will be drawn. @@ -55,6 +58,14 @@ namespace Avalonia.Rendering.SceneGraph /// public Rect DestRect { get; } + /// + /// Gets the bitmap interpolation mode. + /// + /// + /// The scaling mode. + /// + public BitmapInterpolationMode BitmapInterpolationMode { get; } + /// /// Determines if this draw operation equals another. /// @@ -63,18 +74,20 @@ namespace Avalonia.Rendering.SceneGraph /// The opacity of the other draw operation. /// The source rect of the other draw operation. /// The dest rect of the other draw operation. + /// The bitmap interpolation mode. /// True if the draw operations are the same, otherwise false. /// /// The properties of the other draw operation are passed in as arguments to prevent /// allocation of a not-yet-constructed draw operation object. /// - public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect) + public bool Equals(Matrix transform, IRef source, double opacity, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode) { return transform == Transform && Equals(source.Item, Source.Item) && opacity == Opacity && sourceRect == SourceRect && - destRect == DestRect; + destRect == DestRect && + bitmapInterpolationMode == BitmapInterpolationMode; } /// @@ -83,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph // TODO: Probably need to introduce some kind of locking mechanism in the case of // WriteableBitmap. context.Transform = Transform; - context.DrawImage(Source, Opacity, SourceRect, DestRect); + context.DrawImage(Source, Opacity, SourceRect, DestRect, BitmapInterpolationMode); } /// diff --git a/src/Gtk/Avalonia.Gtk3/KeyTransform.cs b/src/Gtk/Avalonia.Gtk3/KeyTransform.cs index 5a34db2e04..69c5a1bf5f 100644 --- a/src/Gtk/Avalonia.Gtk3/KeyTransform.cs +++ b/src/Gtk/Avalonia.Gtk3/KeyTransform.cs @@ -33,17 +33,24 @@ namespace Avalonia.Gtk.Common { GdkKey.Prior, Key.Prior }, //{ GdkKey.?, Key.PageDown } { GdkKey.End, Key.End }, + { GdkKey.KP_End, Key.End }, { GdkKey.Home, Key.Home }, + { GdkKey.KP_Home, Key.Home }, { GdkKey.Left, Key.Left }, + { GdkKey.KP_Left, Key.Left }, { GdkKey.Up, Key.Up }, + { GdkKey.KP_Up, Key.Up }, { GdkKey.Right, Key.Right }, + { GdkKey.KP_Right, Key.Right }, { GdkKey.Down, Key.Down }, + { GdkKey.KP_Down, Key.Down }, { GdkKey.Select, Key.Select }, { GdkKey.Print, Key.Print }, { GdkKey.Execute, Key.Execute }, //{ GdkKey.?, Key.Snapshot } { GdkKey.Insert, Key.Insert }, { GdkKey.Delete, Key.Delete }, + { GdkKey.KP_Delete, Key.Delete }, { GdkKey.Help, Key.Help }, //{ GdkKey.?, Key.D0 } //{ GdkKey.?, Key.D1 } 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/Converters/AvaloniaPropertyTypeConverter.cs b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs index 63b7811dbc..8a0ba64582 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaPropertyTypeConverter.cs @@ -4,19 +4,19 @@ using System; using System.ComponentModel; using System.Globalization; -using System.Text.RegularExpressions; +using Avalonia.Controls; +using Avalonia.Logging; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.Templates; using Avalonia.Styling; -using Portable.Xaml; +using Avalonia.Utilities; 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,39 +24,47 @@ 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) ?? - context.GetFirstAmbientValue()?.TargetType ?? - context.GetFirstAmbientValue + + +"; + 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)); + } + } + + [Fact] + public void Binding_To_TextBlock_Text_With_StringConverter_Works() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + textBlock.DataContext = new { Foo = "world" }; + window.ApplyTemplate(); + + Assert.Equal("Hello world", textBlock.Text); + } + } } -} \ No newline at end of file +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs index 095aae7742..c6fe79bc0c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BindingTests_RelativeSource.cs +++ b/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 = @" + + + +